Repository: sparkmicro/Ki-nTree Branch: main Commit: fb96b0cfcc4f Files: 103 Total size: 497.4 KB Directory structure: gitextract_ks9t75m5/ ├── .coveragerc ├── .github/ │ └── workflows/ │ └── test_deploy.yaml ├── .gitignore ├── LICENSE ├── README.md ├── invoke.yaml ├── kintree/ │ ├── __init__.py │ ├── common/ │ │ ├── part_tools.py │ │ ├── progress.py │ │ └── tools.py │ ├── config/ │ │ ├── automationdirect/ │ │ │ ├── automationdirect_api.yaml │ │ │ └── automationdirect_config.yaml │ │ ├── config_interface.py │ │ ├── digikey/ │ │ │ ├── digikey_api.yaml │ │ │ ├── digikey_categories.yaml │ │ │ └── digikey_config.yaml │ │ ├── element14/ │ │ │ ├── element14_api.yaml │ │ │ └── element14_config.yaml │ │ ├── inventree/ │ │ │ ├── categories.yaml │ │ │ ├── inventree_dev.yaml │ │ │ ├── inventree_prod.yaml │ │ │ ├── parameters.yaml │ │ │ ├── parameters_filters.yaml │ │ │ ├── stock_locations.yaml │ │ │ ├── supplier_parameters.yaml │ │ │ └── suppliers.yaml │ │ ├── jameco/ │ │ │ ├── jameco_api.yaml │ │ │ └── jameco_config.yaml │ │ ├── kicad/ │ │ │ ├── kicad.yaml │ │ │ └── kicad_map.yaml │ │ ├── lcsc/ │ │ │ ├── lcsc_api.yaml │ │ │ └── lcsc_config.yaml │ │ ├── mouser/ │ │ │ ├── mouser_api.yaml │ │ │ └── mouser_config.yaml │ │ ├── settings.py │ │ ├── tme/ │ │ │ ├── tme_api.yaml │ │ │ └── tme_config.yaml │ │ └── user/ │ │ ├── general.yaml │ │ ├── internal_part_number.yaml │ │ └── search_api.yaml │ ├── database/ │ │ ├── inventree_api.py │ │ └── inventree_interface.py │ ├── gui/ │ │ ├── gui.py │ │ └── views/ │ │ ├── common.py │ │ ├── main.py │ │ └── settings.py │ ├── kicad/ │ │ ├── kicad_interface.py │ │ ├── kicad_symbol.py │ │ ├── templates/ │ │ │ ├── LICENSE │ │ │ ├── capacitor-polarized.kicad_sym │ │ │ ├── capacitor.kicad_sym │ │ │ ├── connector.kicad_sym │ │ │ ├── crystal-2p.kicad_sym │ │ │ ├── default.kicad_sym │ │ │ ├── diode-led.kicad_sym │ │ │ ├── diode-schottky.kicad_sym │ │ │ ├── diode-standard.kicad_sym │ │ │ ├── diode-zener.kicad_sym │ │ │ ├── eeprom-sot23.kicad_sym │ │ │ ├── ferrite-bead.kicad_sym │ │ │ ├── fuse.kicad_sym │ │ │ ├── inductor.kicad_sym │ │ │ ├── integrated-circuit.kicad_sym │ │ │ ├── library_template.kicad_sym │ │ │ ├── oscillator-4p.kicad_sym │ │ │ ├── protection-unidir.kicad_sym │ │ │ ├── resistor-sm.kicad_sym │ │ │ ├── resistor.kicad_sym │ │ │ ├── transistor-nfet.kicad_sym │ │ │ ├── transistor-npn.kicad_sym │ │ │ ├── transistor-pfet.kicad_sym │ │ │ └── transistor-pnp.kicad_sym │ │ └── templates_project/ │ │ ├── templates_project.kicad_pcb │ │ ├── templates_project.kicad_prl │ │ ├── templates_project.kicad_pro │ │ └── templates_project.kicad_sch │ ├── kintree_gui.py │ ├── search/ │ │ ├── automationdirect_api.py │ │ ├── digikey_api.py │ │ ├── element14_api.py │ │ ├── jameco_api.py │ │ ├── lcsc_api.py │ │ ├── mouser_api.py │ │ ├── search_api.py │ │ ├── snapeda_api.py │ │ └── tme_api.py │ └── setup_inventree.py ├── kintree_gui.py ├── poetry.toml ├── pyproject.toml ├── requirements.txt ├── run_tests.py ├── setup.cfg ├── tasks.py └── tests/ ├── files/ │ ├── FOOTPRINTS/ │ │ └── RF.pretty/ │ │ ├── Skyworks_SKY13575_639LF.kicad_mod │ │ └── Skyworks_SKY65404-31.kicad_mod │ ├── SYMBOLS/ │ │ └── TEST.kicad_sym │ ├── digikey_config.yaml │ ├── inventree_default_db.sqlite3 │ ├── inventree_dev.yaml │ ├── kicad_map.yaml │ └── results.tgz └── test_samples.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] omit = # Do not run coverage on environment files *env* # Skip GUI coverage kintree/kintree_gui.py kintree/gui/* kintree/common/progress.py # Skip test script run_tests.py ================================================ FILE: .github/workflows/test_deploy.yaml ================================================ name: tests | linting | publishing on: push: branches: - main tags: - "*.*.*" paths-ignore: - README.md - images/** pull_request: branches: - main jobs: style: name: Style checks runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout code uses: actions/checkout@v2 - name: set up python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install -U flake8 invoke - name: PEP checks run: > invoke style tests: name: Integration tests runs-on: ubuntu-latest env: INVENTREE_DB_ENGINE: django.db.backends.sqlite3 INVENTREE_DB_NAME: ${{ github.workspace }}/InvenTree/inventree_default_db.sqlite3 INVENTREE_MEDIA_ROOT: ${{ github.workspace }}/InvenTree INVENTREE_STATIC_ROOT: ${{ github.workspace }}/InvenTree/static INVENTREE_BACKUP_DIR: ${{ github.workspace }}/InvenTree/backup INVENTREE_ENV: 0 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TOKEN_DIGIKEY: ${{ secrets.TOKEN_DIGIKEY }} DIGIKEY_CLIENT_ID: ${{ secrets.DIGIKEY_CLIENT_ID }} DIGIKEY_CLIENT_SECRET: ${{ secrets.DIGIKEY_CLIENT_SECRET }} DIGIKEY_LOCAL_SITE: US DIGIKEY_LOCAL_LANGUAGE: en DIGIKEY_LOCAL_CURRENCY: USD TME_API_TOKEN: ${{ secrets.TME_API_TOKEN }} TME_API_SECRET: ${{ secrets.TME_API_SECRET }} continue-on-error: true strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout code uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | sudo apt install sqlite3 pip install -U pip invoke coveralls - name: InvenTree setup run: | git clone https://github.com/inventree/InvenTree/ mkdir InvenTree/static cp tests/files/inventree_default_db.sqlite3 InvenTree/ cd InvenTree/ && git switch stable && invoke install && invoke migrate && cd - - name: Ki-nTree setup run: | invoke install mkdir -p ~/.config/kintree/user/ && mkdir -p ~/.config/kintree/cache/search/ cp tests/files/inventree_dev.yaml ~/.config/kintree/user/ cp tests/files/kicad_map.yaml ~/.config/kintree/user/ cp tests/files/digikey_config.yaml ~/.config/kintree/user/ cp tests/files/results.tgz ~/.config/kintree/cache/search/ cd ~/.config/kintree/cache/search/ && tar xvf results.tgz && cd - - name: GUI test run: | python kintree_gui.py b > gui.log 2>&1 & sleep 2 cat gui.log export len_log=$(cat gui.log | wc -l) [[ ${len_log} -eq 0 ]] && true || false - name: Setup Digi-Key token if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }} run: | git clone https://$TOKEN_DIGIKEY@github.com/eeintech/digikey-token.git cd digikey-token/ python digikey_token_refresh.py git config --global user.email "kintree@github.actions" git config --global user.name "Ki-nTree Github Actions" git add -u git diff-index --quiet HEAD || git commit -m "Update token" git push origin master cp token_storage.json ~/.config/kintree/cache/ dk_token=$(cat ~/.config/kintree/cache/token_storage.json) echo -e "Digi-Key Token: $dk_token\n" cd .. - name: Run tests if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }} run: | invoke test -e 1 env: MOUSER_PART_API_KEY: ${{ secrets.MOUSER_PART_API_KEY }} ELEMENT14_PART_API_KEY: ${{ secrets.ELEMENT14_PART_API_KEY }} - name: Run tests (skip APIs) if: ${{ github.ref != 'refs/heads/main' && github.event.pull_request.head.repo.full_name != 'sparkmicro/Ki-nTree' }} run: | invoke test -e 0 - name: Coveralls if: ${{ github.ref == 'refs/heads/main' || github.event.pull_request.head.repo.full_name == 'sparkmicro/Ki-nTree' }} run: | coveralls --version coveralls --service=github - name: Run build run: | invoke build test-publish: name: Publish to Test PyPI, then PyPI if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest needs: - style - tests steps: - name: Checkout code uses: actions/checkout@v2 - name: Alter the version in pyproject.toml and overwrite __version__ run: > GTAG=$(echo $REF | sed -e 's#.*/##') && sed --in-place --expression "s/version = \".*\" # placeholder/version = \"$GTAG\"/g" pyproject.toml && echo "__version__ = '$GTAG'" > kintree/__init__.py env: REF: ${{ github.ref }} - name: Display the inferred version run: | head pyproject.toml head kintree/__init__.py - name: Set up Python 3.10 uses: actions/setup-python@v2 with: python-version: '3.10' - name: Install dependencies run: pip install -U poetry - name: Install poetry dependencies run: poetry install --no-root --no-interaction - name: Build the package run: poetry build --no-interaction - name: Set up TestPyPI repo in poetry run: poetry config repositories.test https://test.pypi.org/legacy/ - name: Publish to Test PyPI run: > poetry publish --repository "test" --username "__token__" --password "$TOKEN_TEST_PYPI" env: TOKEN_TEST_PYPI: ${{ secrets.TOKEN_TEST_PYPI }} - name: Publish to PyPI run: > poetry publish --username "__token__" --password "$TOKEN_PYPI" env: TOKEN_PYPI: ${{ secrets.TOKEN_PYPI }} ================================================ FILE: .gitignore ================================================ # Build files dist/ build/ *.spec # Cache and backup files *__pycache__* *.bck # Test files kintree/tests/* .coverage htmlcov/ .vscode/launch.json ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Ki-nTree ### Fast part creation for [KiCad](https://kicad.org/) and [InvenTree](https://inventree.org/) [![License: GPL v3.0](https://img.shields.io/badge/license-GPL_v3.0-green.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Python Versions](https://raw.githubusercontent.com/sparkmicro/Ki-nTree/main/images/python_versions.svg)](https://www.python.org/) [![PyPI](https://img.shields.io/pypi/v/kintree)](https://pypi.org/project/kintree/) [![Tests | Linting | Publishing](https://github.com/sparkmicro/Ki-nTree/actions/workflows/test_deploy.yaml/badge.svg)](https://github.com/sparkmicro/Ki-nTree/actions) [![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) ## :warning: InvenTree Compatibility InvenTree `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) ## :fast_forward: [Demo Video](https://youtu.be/YeWBqOCb4pw) ## Introduction Ki-nTree (pronounced "Key Entry" or "Key 'n' Tree") aims to: * automate part creation of KiCad library parts * automate part creation of InvenTree parts * synchronize parts data between KiCad and InvenTree Ki-nTree works with: - [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 - 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) - the reliable and SCM-friendly KiCad file parser [KiUtils](https://github.com/mvnmgrx/kiutils) built and maintained by [@mvnmgrx](https://github.com/mvnmgrx) - the amazing [Digi-Key API python library](https://github.com/peeter123/digikey-api) built and maintained by [@peeter123](https://github.com/peeter123) - the [Mouser Python API](https://github.com/sparkmicro/mouser-api/) built and maintained by [@eeintech](https://github.com/eeintech) > :warning: **Important Note** > > Ki-nTree version `1.2.x` and forward support Digi-Key API version **4 only**. > > Ki-nTree version `1.0.x` and forward support KiCad versions **6 and up**. > > Ki-nTree versions `0.5.x` and `0.6.x` only support KiCad version **6** (`pip install kintree==0.6.6`). > > To use with KiCad version **5**, use older Ki-nTree `0.4.x` versions (`pip install kintree==0.4.8`). Ki-nTree was developed by [@eeintech](https://github.com/eeintech) for [SPARK Microsystems](https://www.sparkmicro.com/), who generously accepted to make it open-source! ## Get Started ### Requirements * Ki-nTree is currently tested for Python 3.9 to 3.12 versions. * 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. > [Here is a video](https://youtu.be/OI1EGEc0Ju0) to help with the different steps * 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" * 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" * on rolling release distributions like Arch Linux some Flet dependencies need to be repaired manually: ``` sudo pacman -S mpv sudo ln -s /usr/lib/libmpv.so /usr/lib/libmpv.so.1 ``` ### Installation (system wide) 1. Install using Pip ``` bash pip install -U kintree ``` 2. Run Ki-nTree ``` bash kintree ``` ### Run in virtual environment (contained) ##### Linux / MacOS Create a virtual environment and activate it with: ``` bash $ python3 -m venv env-kintree $ source env-kintree/bin/activate ``` Then follow the steps from the [installation section](#installation-system-wide). ##### Windows In Git Bash, use the following commands to create and activate a virtual environment: ``` bash $ python3 -m venv env-kintree $ source env-kintree/Scripts/activate ``` For any other Windows terminal, refer to the [official documentation](https://docs.python.org/library/venv.html#creating-virtual-environments) ### Packages #### Arch Linux Ki-nTree is [available on Arch Linux's AUR](https://aur.archlinux.org/packages/python-kintree/) as `python-kintree`. ### Usage Instructions #### Before Starting If 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: > :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). ``` bash python3 -m kintree.setup_inventree ``` If the InvenTree category tree is **not setup** before starting to use Ki-nTree, you **won't be able to add parts** to InvenTree. #### Advanced Configuration Configuration files are stored in the folder pointed by the `Configuration Files Folder` path in the "User Settings" window:
Click here to read about configuration files

Ki-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. Below is a summary table of the different configuration files, their function and if they are updated by the GUI: | Filename | Function | GUI Update? | | --- | --- | --- | | `categories.yaml` | InvenTree categories hierarchy tree and category codes for IPN generation (see [Before Starting section](#before-starting)) | :x: | | `general.yaml` | General user settings | :heavy_check_mark: | | `internal_part_number.yaml` | Controls for IPN generation | :heavy_check_mark: | | `inventree_.yaml` | InvenTree login credentials, per environment (`=['dev', 'prod']`) | :heavy_check_mark: | | `kicad.yaml` | KiCad symbol, footprint and library paths | :heavy_check_mark: | | `kicad_map.yaml` | Mapping between InvenTree parent categories and KiCad symbol/footprint libraries and templates | :x: | | `parameters.yaml` | List of InvenTree parameters templates (see [InvenTree Part Parameters documentation](https://docs.inventree.org/en/latest/part/parameter/)) | :x: | | `parameters_filters.yaml` | Mapping between InvenTree parent categories and InvenTree parameters templates | :x: | | `search_api.yaml` | Generic controls for Supplier search APIs like cache validity | :heavy_check_mark: | | `supplier_parameters.yaml` | Mapping between InvenTree parameters templates and suppliers parameters/attributes, sorted by InvenTree parent categories (see [Part Parameters section](#part-parameters)) | :x: | | `_config.yaml` | Mapping for supplier name and search results fields, to overwrite defaults (`=['digikey', 'element14', 'lcsc', 'mouser']`) | :x: | | `_api.yaml` | Required supplier API fields, custom to each supplier (`=['digikey', 'element14', 'lcsc', 'mouser']`) | :heavy_check_mark: | | `digikey_categories.yaml` | Mapping between InvenTree categories and Digi-Key categories | :x: | | `digikey_parameters.yaml` | Mapping between InvenTree parameters and Digi-Key parameters/attributes | :x: | > Ki-nTree only supports matching between InvenTree and Digi-Key categories and parameters/attributes (help wanted!)

#### InvenTree Permissions Each InvenTree user has a set of permissions associated to them. Please refer to the [InvenTree documentation](https://inventree.readthedocs.io/en/latest/settings/permissions/) to understand how to setup user permissions. The minimum set of user/group permission required to add parts to InvenTree is: - "Part - Add" to add InvenTree parts - "Purchase Orders - Add" to add manufacturers, suppliers and their associated parts If you wish to automatically add subcategories while creating InvenTree parts, you will need to enable the "Part Categories - Add" permission. Note that each time you enable the "Add" permission to an object, InvenTree automatically enables the "Change" permission to that object too. #### Settings 1. 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)) 2. Click on "Settings > Supplier > Mouser" and fill in the Mouser part search API key 3. Click on "Settings > Supplier > Element14" and fill in the Element14 product search API key (key is shared with Farnell and Newark) 4. Click on "Settings > KiCad", browse to the location where KiCad symbol, template and footprint libraries are stored on your computer then click "Save" 5. 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) 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. 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/`. 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 d. It is also possible to sync the prices in InvenTree with the latest supplier prices. For this enable "Upload Pricing Data to InvenTree" 6. 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 > Note: All URLs should start with "http://" if they do not have a valid SSL certificate. #### Get Digi-Key API token
Show steps (click to expand)

Enter your Digi-Key developer account credentials then login. The following page will appear (`user@email.com` will show your email address): Click on "Allow", another page will open. Click on the "Advanced" button, then click on "Proceed to localhost (unsafe)" at the bottom of the page: > 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) Lastly, a new page will open with a "You may now close this window." message, proceed to get the token.

#### Part Parameters Ki-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: ``` yaml CATEGORY_NAME: INVENTREE_PARAMETER_NAME: - SUPPLIER_1_PARAMETER_NAME_1 - SUPPLIER_1_PARAMETER_NAME_2 - SUPPLIER_2_PARAMETER_NAME_1 ``` It 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. A 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. Refer to [this file](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/config/inventree/supplier_parameters.yaml) as a starting point / example. #### Part Number Search Ki-nTree currently supports APIs for the following electronics suppliers: Digi-Key, Mouser, Element14, TME and LCSC. 1. 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 2. 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) 3. 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. 4. Then, go to the Inventree tabl to pick the `Category` and `Subcategory` to use for this part 5. 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 6. 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 If 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. #### Kicad Templates The automatic part generation in KiCad is controlled via templates: * Template examples are shipped together with Ki-nTree, these can be adjusted to your liking or you also can create completely new ones. * Each template has its own library file where the file name defines the templates name. * 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. * 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. Enjoy! *For any problem/bug you find, please [report an issue](https://github.com/sparkmicro/Ki-nTree/issues).* ## Development ### Requirements You need `python>=3.9` and `poetry`. You 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. ### Setup and run 1. Clone this repository ``` bash git clone https://github.com/sparkmicro/Ki-nTree ``` 2. Install the requirements into a `poetry`-managed virtual environment ``` bash poetry install Installing dependencies from lock file ... Installing the current project: kintree (1.1.99) ``` > Note: the version is not accurate (placeholder only) 3. Run Ki-nTree in the virtual environment ```bash poetry run python -m kintree_gui ``` or ```bash $ poetry shell $ python -m kintree_gui ``` #### Build 1. Make sure you followed the previous installation steps, then run: ``` bash $ poetry build Building kintree (1.1.99) - Building sdist - Built kintree-1.1.99.tar.gz - Building wheel - Built kintree-1.1.99-py3-none-any.whl ``` 2. Exit the virtual environment (`Ctrl + D` on Linux; you can also close the terminal and reopen it in the same folder). Run `pip install dist/.whl` with the file name from the previous step. For example: ```bash pip install dist/kintree-1.1.99-py3-none-any.whl ``` 3. You can now start Ki-nTree by typing `kintree` in the terminal, provided that your python dist path is a part of your `$PATH`. ## License The 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: * https://github.com/mvnmgrx/kiutils * https://github.com/peeter123/digikey-api The [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/)). ================================================ FILE: invoke.yaml ================================================ debug: true run: echo: false ================================================ FILE: kintree/__init__.py ================================================ # WARNING: This file is overwriten when publishing to PyPI # __version__ refers to the tag version instead # VERSION INFORMATION version_info = { 'MAJOR_REVISION': 1, 'MINOR_REVISION': 2, 'RELEASE_STATUS': '1', } __version__ = '.'.join([str(v) for v in version_info.values()]) ================================================ FILE: kintree/common/part_tools.py ================================================ import re from ..config import settings from ..config import config_interface from .tools import cprint def generate_part_number(category: str, part_pk: int, category_code='') -> str: ''' Generate Internal Part Number (IPN) ''' ipn_elements = [] # Prefix if settings.CONFIG_IPN.get('IPN_ENABLE_PREFIX', False): ipn_elements.append(settings.CONFIG_IPN.get('IPN_PREFIX', '')) # Category code if settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False): if not category_code: CATEGORY_CODES = config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES'] try: category_code = CATEGORY_CODES.get(category, '') except AttributeError: category_code = None if category_code: ipn_elements.append(category_code) # Unique ID (mandatory) try: unique_id = str(part_pk).zfill(int(settings.CONFIG_IPN.get('IPN_UNIQUE_ID_LENGTH', '6'))) except: return None ipn_elements.append(unique_id) # Suffix if settings.CONFIG_IPN.get('IPN_ENABLE_SUFFIX', False): ipn_elements.append(settings.CONFIG_IPN.get('IPN_SUFFIX', '')) # Build IPN ipn = '-'.join(ipn_elements) return ipn def compare(new_part_parameters: dict, db_part_parameters: dict, include_filters: list) -> bool: ''' Compare two InvenTree parts based on parameters (specs) ''' try: for parameter, value in new_part_parameters.items(): # Check for filters if include_filters: # Compare only parameters present in include_filters if parameter in include_filters and value != db_part_parameters[parameter]: return False else: # Compare all parameters if value != db_part_parameters[parameter]: return False except KeyError: cprint('[INFO]\tWarning: Failed to compare part with database', silent=settings.HIDE_DEBUG) return False return True def clean_parameter_value(category: str, name: str, value: str) -> str: ''' Clean-up parameter value for consumption in InvenTree and KiCad ''' category = category.lower() name = name.lower() # Parameter specific filters # Package if 'package' in name and 'size' not in name: space_split = value.split() # Return value before the space if len(space_split) > 1: value = space_split[0].replace(',', '') # Sizes if 'size' in name or \ 'height' in name or \ 'pitch' in name or \ 'outline' in name: # imperial = re.findall('[.0-9]*"', value) metric = re.findall('[.0-9]*mm', value) len_metric = len(metric) # Return only the metric dimensions if len_metric > 0 and len_metric <= 1: # One dimension if 'dia' in value.lower(): # Check if diameter value value = '⌀' + metric[0] else: value = metric[0] elif len_metric > 1 and len_metric <= 2: # Two dimensions value = metric[0].replace('mm', '') + 'x' + metric[1] elif len_metric > 2 and len_metric <= 3: # Three dimensions value = metric[0].replace('mm', '') + 'x' + metric[1].replace('mm', '') + 'x' + metric[2] # Power if 'power' in name: # decimal = re.findall('[0-9]\.[0-9]*W', value) ratio = re.findall('[0-9]/[0-9]*W', value) # Return ratio if len(ratio) > 0: value = ratio[0] # ESR, DCR, RDS if 'esr' in name or \ 'dcr' in name or \ 'rds' in name: value = value.replace('Max', '').replace(' ', '').replace('Ohm', 'R') # Category specific filters # RESISTORS if 'resistor' in category: if 'resistance' in name: space_split = value.split() if len(space_split) > 1: resistance = space_split[0] unit = space_split[1] unit_filter = ['kOhms', 'MOhms', 'GOhms'] if unit in unit_filter: unit = unit.replace('Ohms', '').upper() else: unit = unit.replace('Ohms', 'R') value = resistance + unit # General filters # Clean-up ranges separator = '~' if separator in value: space_split = value.split() first_value = space_split[0] if len(space_split) > 2: second_value = space_split[2] # Substract digits, negative sign, points from first value to get unit unit = first_value.replace(re.findall('[-.0-9]*', first_value)[0], '') if unit: value = first_value.replace(unit, '') + separator + second_value # Remove parenthesis section if '(' in value: parenthesis = re.findall(r'\(.*\)', value) if parenthesis: for item in parenthesis: value = value.replace(item, '') # Remove leftover spaces value = value.replace(' ', '') # Remove spaces (for specific cases) if '@' in value: value = value.replace(' ', '') # Escape double-quote (else causes library error in KiCad) if '"' in value: value = value.replace('"', '\\"') # cprint(value) return value ================================================ FILE: kintree/common/progress.py ================================================ import time CREATE_PART_PROGRESS: float MAX_PROGRESS = 1.0 DEFAULT_PROGRESS = 0.1 def reset_progress_bar(progress_bar) -> bool: ''' Reset progress bar ''' global CREATE_PART_PROGRESS # Reset progress CREATE_PART_PROGRESS = 0 progress_bar.color = None progress_bar.value = 0 progress_bar.update() time.sleep(0.1) return True def progress_increment(inc): ''' Increment progress ''' global CREATE_PART_PROGRESS, MAX_PROGRESS if CREATE_PART_PROGRESS + inc < MAX_PROGRESS: CREATE_PART_PROGRESS += inc else: CREATE_PART_PROGRESS = MAX_PROGRESS return CREATE_PART_PROGRESS def update_progress_bar(progress_bar, increment=0) -> bool: ''' Update progress bar during part creation ''' global DEFAULT_PROGRESS if not progress_bar: return True if increment: inc = increment else: # Default inc = DEFAULT_PROGRESS current_value = progress_bar.value * 100 new_value = progress_increment(inc) * 100 # Smooth progress for i in range(int(new_value - current_value)): progress_bar.value += i / 100 progress_bar.update() time.sleep(0.05) return True ================================================ FILE: kintree/common/tools.py ================================================ import builtins import json import os from shutil import copyfile # CUSTOM PRINT METHOD class pcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' ERROR = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' # Overload print function with custom pretty-print def cprint(*args, **kwargs): # Check if silent is set try: silent = kwargs.pop('silent') except: silent = False if not silent: if type(args[0]) is dict: return builtins.print(json.dumps(*args, **kwargs, indent=4, sort_keys=True)) else: try: args = list(args) if 'warning' in args[0].lower(): args[0] = f'{pcolors.WARNING}{args[0]}{pcolors.ENDC}' elif 'error' in args[0].lower(): args[0] = f'{pcolors.ERROR}{args[0]}{pcolors.ENDC}' elif 'fail' in args[0].lower(): args[0] = f'{pcolors.ERROR}{args[0]}{pcolors.ENDC}' elif 'success' in args[0].lower(): args[0] = f'{pcolors.OKGREEN}{args[0]}{pcolors.ENDC}' elif 'pass' in args[0].lower(): args[0] = f'{pcolors.OKGREEN}{args[0]}{pcolors.ENDC}' elif 'main' in args[0].lower(): args[0] = f'{pcolors.HEADER}{args[0]}{pcolors.ENDC}' elif 'skipping' in args[0].lower(): args[0] = f'{pcolors.BOLD}{args[0]}{pcolors.ENDC}' args = tuple(args) except: pass return builtins.print(*args, **kwargs, flush=True) ### def create_library(library_path: str, symbol: str, template_lib: str): ''' Create library files if they don\'t exist ''' if not os.path.exists(library_path): os.mkdir(library_path) new_kicad_sym_file = os.path.join(library_path, f'{symbol}.kicad_sym') if not os.path.exists(new_kicad_sym_file): copyfile(template_lib, new_kicad_sym_file) def get_image_with_retries(url, headers, retries=3, wait=5, silent=False): """ Method to download image with cloudscraper library and retry attempts""" import cloudscraper import time scraper = cloudscraper.create_scraper() for attempt in range(retries): try: response = scraper.get(url, headers=headers, timeout=wait) if response.status_code == 200: return response else: cprint(f'[INFO]\tWarning: Image download Attempt {attempt + 1} failed with status code {response.status_code}. Retrying in {wait} seconds...', silent=silent) except Exception as e: cprint(f'[INFO]\tWarning: Image download Attempt {attempt + 1} encountered an error: {e}. Retrying in {wait} seconds...', silent=silent) time.sleep(wait) cprint('[INFO]\tWarning: All Image download attempts failed. Could not retrieve the image.', silent=silent) return None def download(url, filetype='API data', fileoutput='', timeout=3, enable_headers=False, requests_lib=False, try_cloudscraper=False, silent=False): ''' Standard method to download URL content, with option to save to local file (eg. images) ''' import socket import urllib.request import requests # A more detailed headers was needed for request to Jameco headers = { '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', 'Accept': 'applicaiton/json,image/webp,image/apng,image/*,*/*;q=0.8', 'Accept-Encoding': 'Accept-Encoding: gzip, deflate, br', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive', 'Cache-Control': 'no-cache', } # Set default timeout for download socket socket.setdefaulttimeout(timeout) if enable_headers and not requests_lib: opener = urllib.request.build_opener() opener.addheaders = list(headers.items()) urllib.request.install_opener(opener) try: if filetype == 'PDF': # some distributors/manufacturers implement # redirects which don't allow direct downloads if 'gotoUrl' in url and 'www.ti.com' in url: mpn = url.split('%2F')[-1] url = f'https://www.ti.com/lit/ds/symlink/{mpn}.pdf' if filetype == 'Image' or filetype == 'PDF': # Enable use of requests library for downloading files (some URLs do NOT work with urllib) if requests_lib: response = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True) if filetype.lower() not in response.headers['Content-Type'].lower(): cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) return None with open(fileoutput, 'wb') as file: file.write(response.content) elif try_cloudscraper: response = get_image_with_retries(url, headers=headers) if filetype.lower() not in response.headers['Content-Type'].lower(): cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) return None with open(fileoutput, 'wb') as file: file.write(response.content) else: (file, headers) = urllib.request.urlretrieve(url, filename=fileoutput) if filetype.lower() not in headers['Content-Type'].lower(): cprint(f'[INFO]\tWarning: {filetype} download returned the wrong file type', silent=silent) return None return file else: # some suppliers work with requests.get(), others need urllib.request.urlopen() try: response = requests.get(url) data_json = response.json() return data_json except requests.exceptions.JSONDecodeError: try: url_data = urllib.request.urlopen(url) data = url_data.read() data_json = json.loads(data.decode('utf-8')) return data_json finally: pass except (socket.timeout, requests.exceptions.ConnectTimeout, requests.exceptions.ReadTimeout): cprint(f'[INFO]\tWarning: {filetype} download socket timed out ({timeout}s)', silent=silent) except (urllib.error.HTTPError, requests.exceptions.ConnectionError): cprint(f'[INFO]\tWarning: {filetype} download failed (HTTP Error)', silent=silent) except (urllib.error.URLError, ValueError, AttributeError): cprint(f'[INFO]\tWarning: {filetype} download failed (URL Error)', silent=silent) except requests.exceptions.SSLError: cprint(f'[INFO]\tWarning: {filetype} download failed (SSL Error)', silent=silent) except FileNotFoundError: cprint(f'[INFO]\tWarning: {os.path.dirname(fileoutput)} folder does not exist', silent=silent) return None def download_with_retry(url: str, full_path: str, silent=False, **kwargs) -> str: ''' Standard method to download image URL to local file ''' if not url: cprint('[INFO]\tError: Missing image URL', silent=silent) return False # Try without headers file = download(url, fileoutput=full_path, silent=silent, **kwargs) if not file: # Try with headers file = download(url, fileoutput=full_path, enable_headers=True, silent=silent, **kwargs) if not file: # Try with requests library file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=True, silent=silent, **kwargs) if not file: # Try with cloudscraper file = download(url, fileoutput=full_path, enable_headers=True, requests_lib=False, try_cloudscraper=True, silent=silent, **kwargs) # Still nothing if not file: return False cprint(f'[INFO]\tDownload success ({url=})', silent=silent) return True ================================================ FILE: kintree/config/automationdirect/automationdirect_api.yaml ================================================ AUTOMATIONDIRECT_API_ROOT_URL: "https://www.automationdirect.com" AUTOMATIONDIRECT_API_URL: "https://www.automationdirect.com/ajax?&fctype=adc.falcon.search.SearchFormCtrl&cmd=AjaxSearch" AUTOMATIONDIRECT_API_SEARCH_QUERY: "&searchquery=" # can be anything but probably best to set to search term AUTOMATIONDIRECT_API_SEARCH_STRING: "&solrQueryString=q%3D" # append search term to this and combine with other params AUTOMATIONDIRECT_API_IMAGE_PATH: "https://cdn.automationdirect.com/images/products/medium/m_" # image path for medium size image on product page ================================================ FILE: kintree/config/automationdirect/automationdirect_config.yaml ================================================ SUPPLIER_DATABASE_NAME: Automation Direct SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/config_interface.py ================================================ import base64 import copy import os from sys import platform import yaml from ..common.tools import cprint FUNCTION_FILTER_KEY = '__' def load_file(file_path: str, silent=True) -> dict: ''' Safe load YAML file ''' try: with open(file_path, 'r') as file: try: data = yaml.safe_load(file) except yaml.YAMLError as exc: print(exc) return None except FileNotFoundError: cprint(f'[ERROR]\tFile {file_path} does not exists!', silent=silent) return None return data def dump_file(data: dict, file_path: str) -> bool: ''' Safe dump YAML file ''' with open(file_path, 'w') as file: try: if platform == "win32": yaml.safe_dump(data, file, default_flow_style=False) else: yaml.safe_dump(data, file, default_flow_style=False, allow_unicode=True) except yaml.YAMLError as exc: print(exc) return False return True def load_user_paths(home_dir='') -> dict: ''' Load user config and cache paths ''' user_settings_file = os.path.join(home_dir, 'settings.yaml') user_config = load_file(user_settings_file) if not user_config: user_config = { 'USER_FILES': os.path.join(home_dir, 'user', ''), 'USER_CACHE': os.path.join(home_dir, 'cache', ''), } dump_file(user_config, user_settings_file) return user_config def load_user_config_files(path_to_root: str, path_to_user_files: str, silent=True) -> bool: ''' Load user configuration files ''' result = True def load_config(path): for template_file in os.listdir(path): filename = os.path.basename(template_file) template_data = load_file(os.path.join(path, filename)) try: user_data = load_file(os.path.join(path_to_user_files, filename)) if list(template_data.keys()) == list(user_data.keys()): # Join user data to template data user_settings = {**template_data, **user_data} else: user_settings = user_data # Warn user about config data discrepancies with template data template_vs_user = set(template_data) - set(user_data) # user_vs_template = set(user_data) - set(template_data) if template_vs_user: print(f'[INFO]\tTEMPLATE "{filename}" configuration file contains the following keys which are NOT in your user settings: {template_vs_user}') # if user_vs_template: # cprint(f'[INFO]\tUSER SETTINGS {filename} configuration file contains the following keys which are NOT in the template: {user_vs_template}', silent=silent) except (TypeError, AttributeError): cprint(f'[INFO]\tCreating new {filename} configuration file', silent=silent) # Config file does not exists user_settings = template_data dump_file(user_settings, os.path.join(path_to_user_files, filename)) for dir in ['user', 'inventree', 'kicad', 'digikey', 'mouser', 'element14', 'lcsc', 'tme', 'jameco', 'automationdirect']: try: # Load configuration config_files = os.path.join(path_to_root, dir, '') load_config(config_files) except FileNotFoundError: cprint(f'[INFO]\tWarning: Failed to load {dir.title()} configuration', silent=silent) result = False return result def load_inventree_user_settings(user_config_path: str) -> dict: ''' Load InvenTree user settings from file ''' user_settings = load_file(user_config_path) try: password = user_settings.get('PASSWORD', None) except AttributeError: return user_settings try: # Use base64 encoding to make password unreadable inside the file user_settings['PASSWORD'] = base64.b64decode(password).decode() except TypeError: user_settings['PASSWORD'] = '' if 'ENABLE_PROXY' not in user_settings: user_settings['ENABLE_PROXY'] = False proxies = user_settings.get('PROXIES', None) if not proxies: user_settings['PROXY'] = '' else: # loading the proxy independent if it is http or https user_settings['PROXY'] = list(proxies.values())[0] if 'DATASHEET_UPLOAD' not in user_settings: user_settings['DATASHEET_UPLOAD'] = False if 'PRICING_UPLOAD' not in user_settings: user_settings['PRICING_UPLOAD'] = False return user_settings def save_inventree_user_settings(enable: bool, server: str, username: str, password: str, enable_proxy: bool, proxies: dict, datasheet_upload: bool, pricing_upload: bool, user_config_path: str): ''' Save InvenTree user settings to file ''' user_settings = {} user_settings['ENABLE'] = enable user_settings['SERVER_ADDRESS'] = server user_settings['USERNAME'] = username # Use base64 encoding to make password unreadable inside the file user_settings['PASSWORD'] = base64.b64encode(password.encode()) user_settings['ENABLE_PROXY'] = enable_proxy user_settings['PROXIES'] = proxies user_settings['DATASHEET_UPLOAD'] = datasheet_upload user_settings['PRICING_UPLOAD'] = pricing_upload return dump_file(user_settings, user_config_path) def load_library_path(user_config_path: str, silent=False): ''' Load KiCad library from KiCad settings file ''' user_settings = load_file(user_config_path) try: if not user_settings['KICAD_SYMBOLS_PATH'] and not silent: print('[INFO]\tEmpty KiCad library path') return user_settings['KICAD_SYMBOLS_PATH'] except: # If not defined: use application root folder return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def add_library_path(user_config_path: str, category: str, symbol_library: str) -> bool: ''' Save KiCad library to KiCad settings file ''' user_settings = load_file(user_config_path) if category: index = category else: index = symbol_library if not user_settings['KICAD_LIBRARIES']: user_settings['KICAD_LIBRARIES'] = {} try: if symbol_library not in user_settings['KICAD_LIBRARIES'][index]: user_settings['KICAD_LIBRARIES'][index].append(symbol_library) except: user_settings['KICAD_LIBRARIES'][index] = [symbol_library] return dump_file(user_settings, user_config_path) def load_libraries_paths(user_config_path: str, library_path: str) -> dict: ''' Construct KiCad library files names and paths from KiCad settings file ''' user_settings = load_file(user_config_path) if not os.path.exists(library_path): return None found_library_files = [] for file in os.listdir(library_path): if file.endswith('.kicad_sym'): found_library_files.append(file.replace('.kicad_sym', '')) symbol_libraries_paths = {} assigned_files = [] try: for category, libraries in user_settings['KICAD_LIBRARIES'].items(): symbol_libraries_paths[category] = {} if libraries: for library in libraries: if library in found_library_files: symbol_libraries_paths[category][library] = library_path + \ library + '.kicad_sym' assigned_files.append(library) except: pass for file in found_library_files: if file not in assigned_files: try: symbol_libraries_paths['uncategorized'].append(file) except: symbol_libraries_paths['uncategorized'] = [file] try: symbol_libraries_paths['uncategorized'] = sorted( symbol_libraries_paths['uncategorized']) except: pass # Check that library paths are loaded path_loaded = False for category, paths in symbol_libraries_paths.items(): if paths: path_loaded = True break if not path_loaded: return None # print(symbol_libraries_paths) return symbol_libraries_paths def load_templates_paths(user_config_path: str, template_path: str) -> dict: ''' Construct KiCad template files names and paths from KiCad settings file ''' symbol_templates_paths = {} if not template_path or not os.path.exists(template_path): return symbol_templates_paths # Load configuration file user_settings = load_file(user_config_path) try: for category in user_settings['KICAD_TEMPLATES'].keys(): for subcategory, file_name in user_settings['KICAD_TEMPLATES'][category].items(): if subcategory == 'Default' and not file_name: file_name = 'default' if file_name: try: symbol_templates_paths[category][subcategory] = template_path + \ file_name + '.kicad_sym' except KeyError: symbol_templates_paths[category] = { subcategory: template_path + file_name + '.kicad_sym' } except: pass return symbol_templates_paths def load_footprint_paths(user_config_path: str, footprint_path: str) -> dict: ''' Construct KiCad footprint folder names and paths from KiCad settings file ''' user_settings = load_file(user_config_path) if not os.path.exists(footprint_path): return None found_library_folders = [item.replace('.pretty', '') for item in os.listdir(footprint_path) if os.path.isdir(footprint_path + item)] footprint_libraries_paths = {} assigned_folders = [] try: for category, libraries in user_settings['KICAD_FOOTPRINTS'].items(): footprint_libraries_paths[category] = {} if libraries: for folder in libraries: footprint_libraries_paths[category][folder] = footprint_path + \ folder + '.pretty' assigned_folders.append(folder) except: pass for folder in found_library_folders: if folder not in assigned_folders: try: footprint_libraries_paths['uncategorized'].append(folder) except: footprint_libraries_paths['uncategorized'] = [folder] # Sort uncategorized library paths footprint_libraries_paths['uncategorized'] = sorted( footprint_libraries_paths.get('uncategorized', [])) return footprint_libraries_paths def add_footprint_library(user_config_path: str, category: str, library_folder: str) -> bool: ''' Add KiCad footprint folder name to KiCad settings file ''' user_settings = load_file(user_config_path) if category: index = category else: index = library_folder if not user_settings['KICAD_FOOTPRINTS']: user_settings['KICAD_FOOTPRINTS'] = {} try: if library_folder not in user_settings['KICAD_FOOTPRINTS'][index]: user_settings['KICAD_FOOTPRINTS'][index].append(library_folder) except: user_settings['KICAD_FOOTPRINTS'][index] = [library_folder] return dump_file(user_settings, user_config_path) def load_supplier_categories(supplier_config_path: str, clean=False) -> dict: ''' Load Supplier category mapping from Supplier settings file ''' supplier_categories = load_file(supplier_config_path) if clean: clean_supplier_categories = copy.deepcopy(supplier_categories) for category in supplier_categories: for subcategory in supplier_categories[category]: if FUNCTION_FILTER_KEY in subcategory: clean_supplier_categories[category][subcategory.replace(FUNCTION_FILTER_KEY, '')] \ = supplier_categories[category][subcategory] del clean_supplier_categories[category][subcategory] return clean_supplier_categories # print(supplier_categories) return supplier_categories def load_supplier_categories_inversed(supplier_config_path: str) -> dict: ''' Load Supplier category mapping from Supplier settings file (inversed relation) ''' supplier_categories = load_file(supplier_config_path) try: supplier_categories_inversed = {} for category in supplier_categories.keys(): if supplier_categories[category]: for user, supplier in supplier_categories[category].items(): # Supplier is list type if supplier: if category not in supplier_categories_inversed.keys(): supplier_categories_inversed[category] = {} for item in supplier: supplier_categories_inversed[category][item] = user except: return None # print(supplier_categories_inversed) return supplier_categories_inversed def sync_inventree_supplier_categories(inventree_config_path: str, supplier_config_path: str) -> dict: ''' Synchronize supplier categories dict from InvenTree categories ''' inventree_categories = load_file(inventree_config_path)['CATEGORIES'] supplier_categories = load_supplier_categories(supplier_config_path, clean=True) updated_supplier_categories = copy.deepcopy(supplier_categories) try: for category in inventree_categories: if category not in supplier_categories.keys(): updated_supplier_categories[category] = inventree_categories[category] except: pass return updated_supplier_categories def add_supplier_category(categories: dict, supplier_config_path: str) -> bool: ''' Add Supplier category mapping to Supplier settings file categories = { 'Capacitors': { 'Tantalum': 'Tantalum Capacitors' } } ''' try: supplier_categories = load_file(supplier_config_path) except: return None for category in categories.keys(): for user_subcategory, supplier_category in categories[category].items(): try: supplier_category_keys = supplier_categories[category].keys() except: supplier_categories[category] = { user_subcategory: [supplier_category]} break # Function filtered inventree_subcategory_filter = FUNCTION_FILTER_KEY + user_subcategory if inventree_subcategory_filter in supplier_category_keys: try: if supplier_category not in supplier_categories[category][inventree_subcategory_filter]: supplier_categories[category][inventree_subcategory_filter].append( supplier_category) break except: pass try: supplier_categories[category][inventree_subcategory_filter] = [ supplier_category] break except: pass else: try: if supplier_category not in supplier_categories[category][user_subcategory]: supplier_categories[category][user_subcategory].append( supplier_category) break except: pass try: supplier_categories[category][user_subcategory] = [ supplier_category] break except: pass return False return dump_file(supplier_categories, supplier_config_path) def load_category_parameters(categories: list, supplier_config_path: str) -> dict: ''' Load Supplier parameters mapping from Supplier settings file ''' def find_parameters(output_dict, category_list): category_parameters = None combined = '' for category in reversed(category_list): if category: combined = category + combined if combined in category_file: category_parameters = category_file[combined] break if category in category_file: category_parameters = category_file[category] break combined = '/' + combined if not category_parameters: return if 'parent' in category_parameters: for parent in category_parameters['parent']: find_parameters(output_dict, [parent]) del category_parameters['parent'] for parameter in category_parameters.keys(): if category_parameters[parameter]: for supplier_parameter in category_parameters[parameter]: output_dict[supplier_parameter] = parameter try: category_file = load_file(supplier_config_path) except: return None category_parameters_inversed = {} find_parameters(category_parameters_inversed, categories) return category_parameters_inversed def load_category_parameters_filters(category: str, supplier_config_path: str) -> list: ''' Load Supplier parameters filters from Supplier settings file ''' try: parameters_filters = load_file(supplier_config_path)[category] except: return [] # print(parameters_filters) return parameters_filters ================================================ FILE: kintree/config/digikey/digikey_api.yaml ================================================ DIGIKEY_CLIENT_ID: '' DIGIKEY_CLIENT_SECRET: '' ================================================ FILE: kintree/config/digikey/digikey_categories.yaml ================================================ Capacitors: Aluminium: - Aluminum Electrolytic Capacitors Ceramic: - Ceramic Capacitors - Ceramic Polymer: - Aluminum - Polymer Capacitors - Tantalum - Polymer Capacitors Super Capacitors: - Electric Double Layer Capacitors (EDLC), Supercapacitors Tantalum: - Tantalum Capacitors Circuit Protections: Fuses: - Fuses PTC: - PTC Resettable Fuses TVS: - TVS - Diodes Connectors: Board-to-Board: - Rectangular Connectors - Arrays, Edge Type, Mezzanine (Board to Board) - Rectangular Connectors - Spring Loaded Coaxial: - Coaxial Connectors (RF) FPC: - FFC, FPC (Flat Flexible) Connectors Header: - Rectangular Connectors - Headers, Male Pins - Rectangular Connectors - Headers, Receptacles, Female Sockets Interface: - USB, DVI, HDMI Connectors - Barrel - Audio Connectors - Memory Connectors - PC Card Sockets - Modular Connectors - Jacks With Magnetics Crystals and Oscillators: Crystals: - Crystals Oscillators: - Oscillators Diodes: LED: - LED Indication - Discrete - Addressable, Specialty Zener: - Diodes - Zener - Single __Schottky: - Diodes - Rectifiers - Single __Standard: - Diodes - Rectifiers - Single Inductors: Ferrite Bead: - Ferrite Beads and Chips Power: - Fixed Inductors Integrated Circuits: Interface: - Interface - CODECs - PMIC - Battery Chargers - Interface - Analog Switches, Multiplexers, Demultiplexers - Interface - Controllers Logic: - Logic - Translators, Level Shifters - Clock/Timing - Clock Generators, PLLs, Frequency Synthesizers - Logic - Buffers, Drivers, Receivers, Transceivers Microcontroller: - Embedded - Microcontrollers Memory: - Memory Sensor: - Humidity, Moisture Sensors - Motion Sensors - IMUs (Inertial Measurement Units) - PMIC - Current Regulation/Management Mechanicals: Standoff: - Board Spacers, Standoffs Switch: - Tactile Switches - Slide Switches Power Management: LDO: - PMIC - Voltage Regulators - Linear __Boost: - PMIC - Voltage Regulators - DC DC Switching Regulators __Buck: - PMIC - Voltage Regulators - DC DC Switching Regulators RF: Antenna: null Chipset: null Filter: - Balun Resistors: Potentiometers: - Potentiometers, Variable Resistors - Rotary Potentiometers, Rheostats Surface Mount: - Chip Resistor - Surface Mount Through Hole: - Through Hole Resistors Transistors: __N-Channel FET: - Transistors - FETs, MOSFETs - Single __NPN: - Transistors - Bipolar (BJT) - Single - Transistors - FETs, MOSFETs - Single __P-Channel FET: - Transistors - FETs, MOSFETs - Single __PNP: - Transistors - Bipolar (BJT) - Single Load Switches: - PMIC - Power Distribution Switches, Load Drivers ================================================ FILE: kintree/config/digikey/digikey_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: Digi-Key SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/element14/element14_api.yaml ================================================ ELEMENT14_PRODUCT_SEARCH_API_KEY: null FARNELL_STORE: null NEWARK_STORE: null ELEMENT14_STORE: null ================================================ FILE: kintree/config/element14/element14_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: null SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/inventree/categories.yaml ================================================ CATEGORIES: Assemblies: Printed-Circuit Board Assembly: null Product: null Capacitors: Aluminium: null Ceramic: '0402': null '0603': null '0805': null Polymer: null Super Capacitors: null Tantalum: null Circuit Protections: Fuses: null PTC: null TVS: null Connectors: Battery: null Board-to-Board: null Coaxial: null FPC: null Header: null Interface: null Crystals and Oscillators: Crystals: null Oscillators: null Diodes: LED: null Schottky: null Standard: null Zener: null Inductors: Ferrite Bead: null Power: null Integrated Circuits: Interface: null Logic: null Memory: null Microcontroller: null Sensor: null Mechanicals: Nuts: null Screws: null Standoff: null Switch: null Miscellaneous: Batteries: null Modules: null Power Management: Boost: null Buck: null LDO: null PMIC: null Printed-Circuit Boards: null RF: Antenna: null Chipset: null Filter: null Shield: null Resistors: NTC: null Potentiometers: null Surface Mount: null Through Hole: null Transistors: Load Switches: null N-Channel FET: null NPN: null P-Channel FET: null PNP: null CODES: Assemblies: PCA Capacitors: CAP Circuit Protections: PRO Connectors: CON Crystals and Oscillators: CLK Diodes: DIO Inductors: IND Integrated Circuits: ICS Mechanicals: MEC Miscellaneous: MIS Modules: MOD Power Management: PWR Printed-Circuit Boards: PCB RF: RFC Resistors: RES Transistors: TRA ================================================ FILE: kintree/config/inventree/inventree_dev.yaml ================================================ ENABLE: true ENABLE_PROXY: false PASSWORD: !!binary | '' PROXIES: null SERVER_ADDRESS: '' USERNAME: '' DATASHEET_UPLOAD: false ================================================ FILE: kintree/config/inventree/inventree_prod.yaml ================================================ ENABLE: true ENABLE_PROXY: false PASSWORD: !!binary | '' PROXIES: null SERVER_ADDRESS: '' USERNAME: '' DATASHEET_UPLOAD: false ================================================ FILE: kintree/config/inventree/parameters.yaml ================================================ # Parameters # Name: Unit Min Output Voltage: V Antenna Type: null B Constant: K Breakdown Voltage: V Capacitance: nF Clamping Voltage: V Collector Gate Voltage: V DC Resistance: "m\u03A9" ESR: "m\u03A9" Footprint: null Forward Voltage: V Frequency: Hz Frequency Stability: ppm Frequency Tolerance: ppm Function Type: null Interface Type: null LED Color: null Load Capacitance: pF Locking: null Mating Height: mm Max Input Voltage: V Max Output Voltage: V Maximum Gate Voltage: V Memory Size: null Min Input Voltage: V Mounting Type: null Number of Channels: null Number of Contacts: null Number of Elements: null Number of Rows: null Orientation: null Output Current: A Output Type: null Package Height: mm Package Size: mm Package Type: null Pitch: mm Polarity: null Quiescent Current: A RDS On Resistance: "\u03A9" RDS On Voltage: V Rated Current: A Rated Power: W Rated Voltage: V Saturation Current: A Shielding: null Standoff Voltage: V Symbol: null Temperature Grade: null Temperature Range: "\xB0C" Tolerance: '%' Value: null ================================================ FILE: kintree/config/inventree/parameters_filters.yaml ================================================ Capacitors: - Value - Rated Voltage - Tolerance - Package Type - Temperature Grade Circuit Protections: - Value Connectors: - Value Crystals and Oscillators: - Value - Package Type - Package Size - Temperature Range Diodes: - Value Inductors: - Value - Rated Current - Package Type - Package Size - Temperature Range Integrated Circuits: - Value Mechanicals: - Value Modules: - null Power Management: - Value Printed-Circuit Boards: - null RF: - Value Resistors: - Value - Tolerance - Rated Power - Package Type - Temperature Range Transistors: - Value ================================================ FILE: kintree/config/inventree/stock_locations.yaml ================================================ STOCK_LOCATIONS: null ================================================ FILE: kintree/config/inventree/supplier_parameters.yaml ================================================ # Parameter Mapping between InvenTree parameter template and suppliers parameters naming # Each template parameter can match to multiple suppliers parameters # Categories (main keys) should match categories in the categories.yaml file # Parameter template names should match those found in the parameters.yaml file Base: Temperature Range: - Operating Temperature Package Type: - Package / Case Passives: Tolerance: - Tolerance Capacitors: parent: - Base - Passives ESR: - ESR (Equivalent Series Resistance) Package Height: - Height - Seated (Max) - Thickness (Max) Package Size: - Size / Dimension Rated Voltage: - Voltage - Rated - Voltage Rated Temperature Grade: - Temperature Coefficient Temperature Range: - Operating Temperature Value: - Capacitance Circuit Protections: parent: - Base Breakdown Voltage: - Voltage - Breakdown (Min) Capacitance: - Capacitance @ Frequency Clamping Voltage: - Voltage - Clamping (Max) @ Ipp Rated Current: - Current Rating (Amps) - Current - Max Rated Power: - Power - Peak Pulse Rated Voltage: - Voltage Rating - DC - Voltage - Max Standoff Voltage: - Voltage - Reverse Standoff (Typ) Value: - Manufacturer Part Number Connectors: Frequency: - Frequency - Max Interface Type: - Connector Type - Flat Flex Type Locking: - Locking Feature - Fastening Type Mating Height: - Mated Stacking Heights Mounting Type: - Mounting Type Number of Contacts: - Number of Contacts - Number of Positions Number of Rows: - Number of Rows Orientation: - Orientation Package Height: - Height Above Board - Insulation Height Pitch: - Pitch - Pitch - Mating Polarity: - Gender Shielding: - Shielding Temperature Range: - Operating Temperature Value: - Manufacturer Part Number Crystals and Oscillators: parent: - Base Frequency Stability: - Frequency Stability Frequency Tolerance: - Frequency Tolerance Load Capacitance: - Load Capacitance Package Height: - Height - Seated (Max) Package Size: - Size / Dimension Rated Current: - Current - Supply (Max) Rated Voltage: - Voltage - Supply Value: - Frequency Diodes: parent: - Base Forward Voltage: - Voltage - Forward (Vf) (Max) @ If - Voltage - Forward (Vf) (Typ) Function Type: - Diode Type LED Color: - Color Rated Current: - Current - Average Rectified (Io) Rated Power: - Power - Max Rated Voltage: - Voltage - DC Reverse (Vr) (Max) - Voltage - Zener (Nom) (Vz) Temperature Range: - Operating Temperature - Junction Value: - Manufacturer Part Number Inductors: parent: - Base - Passives ESR: - DC Resistance (DCR) - DC Resistance (DCR) (Max) Package Height: - Height - Seated (Max) - Height (Max) Package Size: - Size / Dimension Rated Current: - Current Rating (Max) - Current Rating (Amps) Saturation Current: - Current - Saturation Shielding: - Shielding Value: - Inductance - Impedance @ Frequency Integrated Circuits: parent: - Base Frequency: - Clock Frequency - Speed - Data Rate - Frequency Range - -3db Bandwidth Function Type: - Translator Type - Technology - Core Processor - Type - Sensor Type Memory Size: - Program Memory Size - Memory Size Number of Channels: - Channels per Circuit Rated Voltage: - Voltage - VCCA - Voltage - VCCB - Voltage - Supply - Voltage - Supply (Vcc/Vdd) - Voltage - Supply, Digital - Voltage - Supply, Single (V+) Value: - Manufacturer Part Number Mechanicals: parent: - Base Function Type: - Circuit - Type Mounting Type: - Mounting Type - Features Package Height: - Between Board Height Package Size: - Outline - Diameter - Outside Package Type: - Screw, Thread Size Rated Current: - Contact Rating @ Voltage Value: - Manufacturer Part Number Power Management: parent: - Base Min Output Voltage: - Voltage - Output (Min/Fixed) Frequency: - Frequency - Switching Function Type: - Topology Max Input Voltage: - Voltage - Input (Max) Max Output Voltage: - Voltage - Output (Max) Min Input Voltage: - Voltage - Input (Min) Output Type: - Output Type Package Type: - Supplier Device Package Quiescent Current: - Current - Quiescent (Iq) Rated Current: - Current - Output Value: - Manufacturer Part Number RF: parent: - Base Frequency: - Frequency Range Function Type: null Rated Voltage: null Value: - Manufacturer Part Number Resistors: parent: - Passives Package Type: - Supplier Device Package Rated Power: - Power (Watts) Temperature Range: - Operating Temperature Value: - Resistance Transistors: Collector-Gate Voltage: - Vce Saturation (Max) @ Ib, Ic - Vgs(th) (Max) @ Id Function Type: - Transistor Type - FET Type Maximum Gate Voltage: - Vgs (Max) Package Type: - Supplier Device Package RDS On Resistance: - Rds On (Max) @ Id, Vgs Rated Current: - Current - Collector (Ic) (Max) - "Current - Continuous Drain (Id) @ 25\xB0C" Rated Power: - Power - Max - Power Dissipation (Max) Rated Voltage: - Voltage - Collector Emitter Breakdown (Max) - Drain to Source Voltage (Vdss) Temperature Range: - Operating Temperature Value: - Manufacturer Part Number ================================================ FILE: kintree/config/inventree/suppliers.yaml ================================================ Digi-Key: enable: true name: Digi-Key Mouser: enable: true name: Mouser Element14: enable: true name: Element14 Farnell: enable: true name: Farnell Jameco: enable: true name: Jameco AutomationDirect: enable: true name: Automation Direct Newark: enable: true name: Newark LCSC: enable: true name: LCSC TME: enable: true name: TME ================================================ FILE: kintree/config/jameco/jameco_api.yaml ================================================ JAMECO_API_URL: https://ahzbkf.a.searchspring.io/api/search/search.json?ajaxCatalog=v3&resultsFormat=native&siteId=ahzbkf&q= ================================================ FILE: kintree/config/jameco/jameco_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: Jameco Electronics SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/kicad/kicad.yaml ================================================ KICAD_SYMBOLS_PATH: '' KICAD_TEMPLATES_PATH: kintree/kicad/templates/ KICAD_FOOTPRINTS_PATH: '' ================================================ FILE: kintree/config/kicad/kicad_map.yaml ================================================ KICAD_FOOTPRINTS: KICAD_LIBRARIES: KICAD_TEMPLATES: Capacitors: Aluminium: capacitor-polarized Ceramic: capacitor Default: capacitor Polymer: capacitor-polarized Super Capacitors: capacitor-polarized Tantalum: capacitor-polarized Circuit Protections: Default: protection-unidir Fuse: fuse TVS: protection-unidir Connectors: Default: connector Crystals and Oscillators: Crystal 2P: crystal-2p Default: crystal-2p Oscillator 4P: oscillator-4p Diodes: Default: diode-standard LED: diode-led Schottky: diode-schottky Standard: diode-standard Zener: diode-zener Inductors: Default: inductor Ferrite Bead: ferrite-bead Power: inductor Integrated Circuits: Default: integrated-circuit Mechanicals: Default: default Power Management: Default: integrated-circuit Resistors: Default: resistor Surface Mount: resistor-sm Through Hole: resistor RF: Default: integrated-circuit Transistors: Default: transistor-nfet N-Channel FET: transistor-nfet NPN: transistor-npn P-Channel FET: transistor-pfet PNP: transistor-pnp ================================================ FILE: kintree/config/lcsc/lcsc_api.yaml ================================================ LCSC_API_URL: https://wmsc.lcsc.com/ftps/wm/product/detail?productCode= ================================================ FILE: kintree/config/lcsc/lcsc_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: LCSC Electronics SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/mouser/mouser_api.yaml ================================================ MOUSER_PART_API_KEY: null ================================================ FILE: kintree/config/mouser/mouser_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: Mouser Electronics SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null EXTRA_FIELDS: null ================================================ FILE: kintree/config/settings.py ================================================ import os import sys import platform from enum import Enum from ..common.tools import cprint from .import config_interface # DEBUG # Testing ENABLE_TEST = False # Silent Mode SILENT = False # Debug HIDE_DEBUG = True def enable_test_mode(): global ENABLE_TEST global SILENT ENABLE_TEST = True SILENT = True # PATHS if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): PROJECT_DIR = os.path.abspath(os.path.dirname(sys.executable)) else: PROJECT_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # InvenTree API sys.path.append(os.path.join(PROJECT_DIR, 'database', 'inventree-python')) # Digi-Key API sys.path.append(os.path.join(PROJECT_DIR, 'search', 'digikey_api')) # KiCad Library Utils sys.path.append(os.path.join(PROJECT_DIR, 'kicad')) # Tests sys.path.append(os.path.join(PROJECT_DIR, 'tests')) # HOME FOLDER USER_HOME = os.path.expanduser("~") # APP NAME APP_NAME = 'kintree' # CONFIG PATH if platform.system() == 'Linux': HOME_DIR = os.path.join(USER_HOME, '.config', APP_NAME, '') else: HOME_DIR = os.path.join(USER_HOME, APP_NAME, '') # Create config path if it does not exists if not os.path.exists(HOME_DIR): os.makedirs(HOME_DIR, exist_ok=True) # USER AND CONFIG FILES def load_user_config(): global USER_SETTINGS global CONFIG_ROOT global CONFIG_USER_FILES USER_SETTINGS = config_interface.load_user_paths(home_dir=HOME_DIR) CONFIG_ROOT = os.path.join(PROJECT_DIR, 'config', '') CONFIG_USER_FILES = os.path.join(USER_SETTINGS['USER_FILES'], '') # Create user files folder if it does not exists if not os.path.exists(CONFIG_USER_FILES): os.makedirs(CONFIG_USER_FILES) # Create user files return config_interface.load_user_config_files(path_to_root=CONFIG_ROOT, path_to_user_files=CONFIG_USER_FILES, silent=HIDE_DEBUG) # Load user config USER_CONFIG_FILE = os.path.join(HOME_DIR, 'settings.yaml') if not load_user_config(): # Check if configuration files already exist if not os.path.isfile(os.path.join(CONFIG_USER_FILES, 'categories.yaml')): cprint('\n[ERROR]\tSome Ki-nTree configuration files seem to be missing') exit(-1) # KiCad KICAD_CONFIG_PATHS = os.path.join(CONFIG_USER_FILES, 'kicad.yaml') KICAD_CONFIG_CATEGORY_MAP = os.path.join(CONFIG_USER_FILES, 'kicad_map.yaml') # Inventree CONFIG_CATEGORIES = os.path.join(CONFIG_USER_FILES, 'categories.yaml') CONFIG_STOCK_LOCATIONS = os.path.join(CONFIG_USER_FILES, 'stock_locations.yaml') CONFIG_PARAMETERS = os.path.join(CONFIG_USER_FILES, 'parameters.yaml') CONFIG_PARAMETERS_FILTERS = os.path.join( CONFIG_USER_FILES, 'parameters_filters.yaml') # INTERNAL PART NUMBERS CONFIG_IPN_PATH = os.path.join(CONFIG_USER_FILES, 'internal_part_number.yaml') def load_ipn_settings(): global CONFIG_IPN CONFIG_IPN = config_interface.load_file(CONFIG_IPN_PATH) load_ipn_settings() # GENERAL SETTINGS CONFIG_GENERAL_PATH = os.path.join(CONFIG_USER_FILES, 'general.yaml') CONFIG_GENERAL = config_interface.load_file(CONFIG_GENERAL_PATH) # Datasheets DATASHEET_SAVE_ENABLED = CONFIG_GENERAL.get('DATASHEET_SAVE_ENABLED', False) DATASHEET_SAVE_PATH = CONFIG_GENERAL.get('DATASHEET_SAVE_PATH', '') # Open Browser AUTOMATIC_BROWSER_OPEN = CONFIG_GENERAL.get('AUTOMATIC_BROWSER_OPEN', False) # Default Supplier DEFAULT_SUPPLIER = CONFIG_GENERAL.get('DEFAULT_SUPPLIER', 'Digi-Key') # Load enable flags def reload_enable_flags(): global ENABLE_KICAD global ENABLE_INVENTREE global ENABLE_ALTERNATE global UPDATE_INVENTREE global CHECK_EXISTING try: ENABLE_KICAD = CONFIG_GENERAL.get('ENABLE_KICAD', False) ENABLE_INVENTREE = CONFIG_GENERAL.get('ENABLE_INVENTREE', False) ENABLE_ALTERNATE = CONFIG_GENERAL.get('ENABLE_ALTERNATE', False) UPDATE_INVENTREE = CONFIG_GENERAL.get('UPDATE_INVENTREE', False) CHECK_EXISTING = CONFIG_GENERAL.get('CHECK_EXISTING', True) return True except TypeError: pass return False reload_enable_flags() # Supported suppliers APIs CONFIG_SUPPLIERS_PATH = os.path.join(CONFIG_USER_FILES, 'suppliers.yaml') CONFIG_SUPPLIERS = config_interface.load_file(CONFIG_SUPPLIERS_PATH) SUPPORTED_SUPPLIERS_API = [] # Load suppliers def load_suppliers(): global CONFIG_SUPPLIERS global SUPPORTED_SUPPLIERS_API update_supplier_config = {} SUPPORTED_SUPPLIERS_API = [] for supplier, data in CONFIG_SUPPLIERS.items(): try: if data['enable']: if data['name']: supplier_name = data['name'].replace(' ', '') SUPPORTED_SUPPLIERS_API.append(supplier_name) else: supplier_key = supplier.replace(' ', '') SUPPORTED_SUPPLIERS_API.append(supplier_key) except (TypeError, KeyError): update_supplier_config[supplier] = { 'enable': True, 'name': supplier, } # Update supplier configuration file if update_supplier_config: config_interface.dump_file({**CONFIG_SUPPLIERS, **update_supplier_config}, CONFIG_SUPPLIERS_PATH) CONFIG_SUPPLIERS = config_interface.load_file(CONFIG_SUPPLIERS_PATH) return False return True if not load_suppliers(): # Re-load updated configuration file load_suppliers() # Generic API user configuration CONFIG_SUPPLIER_PARAMETERS = os.path.join(CONFIG_USER_FILES, 'supplier_parameters.yaml') CONFIG_SEARCH_API_PATH = os.path.join(CONFIG_USER_FILES, 'search_api.yaml') CONFIG_SEARCH_API = config_interface.load_file(CONFIG_SEARCH_API_PATH) # Digi-Key user configuration CONFIG_DIGIKEY = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'digikey_config.yaml')) CONFIG_DIGIKEY_API = os.path.join(CONFIG_USER_FILES, 'digikey_api.yaml') CONFIG_DIGIKEY_CATEGORIES = os.path.join(CONFIG_USER_FILES, 'digikey_categories.yaml') # Mouser user configuration CONFIG_MOUSER = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'mouser_config.yaml')) CONFIG_MOUSER_API = os.path.join(CONFIG_USER_FILES, 'mouser_api.yaml') # Element14 user configuration (includes Farnell, Newark and Element14) CONFIG_ELEMENT14 = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'element14_config.yaml')) CONFIG_ELEMENT14_API = os.path.join(CONFIG_USER_FILES, 'element14_api.yaml') # LCSC user configuration CONFIG_LCSC = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'lcsc_config.yaml')) CONFIG_LCSC_API = os.path.join(CONFIG_USER_FILES, 'lcsc_api.yaml') # JAMECO user configuration CONFIG_JAMECO = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'jameco_config.yaml')) CONFIG_JAMECO_API = os.path.join(CONFIG_USER_FILES, 'jameco_api.yaml') # AUTOMATIONDIRECT user configuration CONFIG_AUTOMATIONDIRECT = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'automationdirect_config.yaml')) CONFIG_AUTOMATIONDIRECT_API = os.path.join(CONFIG_USER_FILES, 'automationdirect_api.yaml') # TME user configuration CONFIG_TME = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'tme_config.yaml')) CONFIG_TME_API = os.path.join(CONFIG_USER_FILES, 'tme_api.yaml') # Automatic category match confidence level (from 0 to 100) CATEGORY_MATCH_RATIO_LIMIT = CONFIG_SEARCH_API.get('CATEGORY_MATCH_RATIO_LIMIT', 100) # Search results caching (stored in files) CACHE_ENABLED = CONFIG_SEARCH_API.get('CACHE_ENABLED', True) # Cache validity in days CACHE_VALID_DAYS = int(CONFIG_SEARCH_API.get('CACHE_VALID_DAYS', '7')) # Caching settings def load_cache_settings(): global search_results global search_images global search_datasheets global CACHE_ENABLED global DIGIKEY_STORAGE_PATH USER_SETTINGS = config_interface.load_user_paths(home_dir=HOME_DIR) search_results = { 'directory': os.path.join(USER_SETTINGS['USER_CACHE'], 'search', ''), 'extension': '.yaml', } # Create folder if it does not exists if not os.path.exists(search_results['directory']): os.makedirs(search_results['directory']) # Part images search_images = os.path.join(USER_SETTINGS['USER_CACHE'], 'images', '') # Create folder if it does not exists if not os.path.exists(search_images): os.makedirs(search_images) # Part images search_datasheets = os.path.join( USER_SETTINGS['USER_CACHE'], 'datasheets', '') # Create folder if it does not exists if not os.path.exists(search_datasheets): os.makedirs(search_datasheets) # API token storage path DIGIKEY_STORAGE_PATH = os.path.join(USER_SETTINGS['USER_CACHE'], '') # Load cache settings load_cache_settings() # KICAD # User Settings KICAD_SETTINGS = {} def load_kicad_settings(): global KICAD_CONFIG_PATHS global KICAD_SETTINGS kicad_user_settings = config_interface.load_file(KICAD_CONFIG_PATHS, silent=False) if kicad_user_settings: KICAD_SETTINGS['KICAD_SYMBOLS_PATH'] = kicad_user_settings.get('KICAD_SYMBOLS_PATH', None) KICAD_SETTINGS['KICAD_TEMPLATES_PATH'] = kicad_user_settings.get('KICAD_TEMPLATES_PATH', None) KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'] = kicad_user_settings.get('KICAD_FOOTPRINTS_PATH', None) # Load kicad settings load_kicad_settings() def set_default_supplier(value: str, save=False): global DEFAULT_SUPPLIER DEFAULT_SUPPLIER = value if save: user_settings = config_interface.load_file(os.path.join(CONFIG_USER_FILES, 'general.yaml')) user_settings['DEFAULT_SUPPLIER'] = value config_interface.dump_file(user_settings, os.path.join(CONFIG_USER_FILES, 'general.yaml')) return # Library Paths if not ENABLE_TEST: symbol_libraries_paths = config_interface.load_libraries_paths( KICAD_CONFIG_CATEGORY_MAP, KICAD_SETTINGS['KICAD_SYMBOLS_PATH'], ) # cprint(symbol_libraries_paths) # Template Paths symbol_templates_paths = config_interface.load_templates_paths( KICAD_CONFIG_CATEGORY_MAP, KICAD_SETTINGS['KICAD_TEMPLATES_PATH'], ) # cprint(symbol_templates_paths) # Footprint Libraries footprint_libraries_paths = config_interface.load_footprint_paths( KICAD_CONFIG_CATEGORY_MAP, KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'], ) # cprint(footprint_libraries_paths) footprint_name_default = 'TBD' AUTO_GENERATE_LIB = True symbol_template_lib = os.path.join( PROJECT_DIR, 'kicad', 'templates', 'library_template.kicad_sym' ) # INVENTREE class Environment(Enum): ''' Server/Remote Development: DEVELOPMENT Server/Remote Production: PRODUCTION ''' DEVELOPMENT = 0 PRODUCTION = 1 # Pick environment environment = CONFIG_GENERAL.get('INVENTREE_ENV', None) environment = os.environ.get('INVENTREE_ENV', environment) try: environment = int(environment) except TypeError: environment = 0 # Load correct user file if environment == Environment.PRODUCTION.value: INVENTREE_CONFIG = os.path.join(CONFIG_USER_FILES, 'inventree_prod.yaml') else: INVENTREE_CONFIG = os.path.join(CONFIG_USER_FILES, 'inventree_dev.yaml') # Load user settings inventree_settings = config_interface.load_inventree_user_settings(INVENTREE_CONFIG) # Server settings def load_inventree_settings(): global SERVER_ADDRESS global USERNAME global PASSWORD global ENABLE_PROXY global PROXIES global PART_URL_ROOT global DATASHEET_UPLOAD global PRICING_UPLOAD inventree_settings = config_interface.load_inventree_user_settings(INVENTREE_CONFIG) SERVER_ADDRESS = inventree_settings.get('SERVER_ADDRESS', None) USERNAME = inventree_settings.get('USERNAME', None) PASSWORD = inventree_settings.get('PASSWORD', None) ENABLE_PROXY = inventree_settings.get('ENABLE_PROXY', False) PROXIES = inventree_settings.get('PROXIES', None) DATASHEET_UPLOAD = inventree_settings.get('DATASHEET_UPLOAD', False) PRICING_UPLOAD = inventree_settings.get('PRICING_UPLOAD', False) # Part URL if SERVER_ADDRESS: # If missing, append slash to root URL root_url = SERVER_ADDRESS if not SERVER_ADDRESS.endswith('/'): root_url = root_url + '/' # Set part URL PART_URL_ROOT = root_url + 'part/' # InvenTree part dictionary template inventree_part_template = { 'name': None, 'description': None, 'IPN': None, 'revision': None, 'keywords': None, 'image': None, 'inventree_url': None, 'manufacturer_name': None, 'manufacturer_part_number': None, 'datasheet': None, 'supplier_name': None, 'supplier_part_number': None, 'supplier_link': None, 'parameters': {}, } # Enable flags def set_enable_flag(key: str, value: bool): global CONFIG_GENERAL user_settings = CONFIG_GENERAL if key in ['kicad', 'inventree', 'alternate', 'update', 'check_existing']: if key == 'kicad': user_settings['ENABLE_KICAD'] = value elif key == 'inventree': user_settings['ENABLE_INVENTREE'] = value elif key == 'alternate': user_settings['ENABLE_ALTERNATE'] = value elif key == 'update': user_settings['UPDATE_INVENTREE'] = value elif key == 'check_existing': user_settings['CHECK_EXISTING'] = value # Save config_interface.dump_file( data=user_settings, file_path=os.path.join(CONFIG_USER_FILES, 'general.yaml'), ) return reload_enable_flags() ================================================ FILE: kintree/config/tme/tme_api.yaml ================================================ TME_API_TOKEN: NULL TME_API_SECRET: NULL TME_API_COUNTRY: US TME_API_LANGUAGE: EN ================================================ FILE: kintree/config/tme/tme_config.yaml ================================================ SUPPLIER_INVENTREE_NAME: TME SEARCH_NAME: null SEARCH_DESCRIPTION: null SEARCH_REVISION: null SEARCH_KEYWORDS: null SEARCH_SKU: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_SUPPLIER_URL: null SEARCH_DATASHEET: null ================================================ FILE: kintree/config/user/general.yaml ================================================ DATASHEET_SAVE_ENABLED: false DATASHEET_SAVE_PATH: null DATASHEET_INVENTREE_ENABLED: false AUTOMATIC_BROWSER_OPEN: true INVENTREE_ENV: null DEFAULT_SUPPLIER: Digi-Key ENABLE_KICAD: false ENABLE_INVENTREE: false ENABLE_ALTERNATE: false CHECK_EXISTING: true ================================================ FILE: kintree/config/user/internal_part_number.yaml ================================================ IPN_ENABLE_CREATE: true IPN_USE_MANUFACTURER_PART_NUMBER: false IPN_PREFIX: null IPN_CATEGORY_CODE: true IPN_UNIQUE_ID_LENGTH: '6' IPN_ENABLE_PREFIX: false IPN_ENABLE_SUFFIX: true IPN_SUFFIX: '00' INVENTREE_DEFAULT_REV: 'A' ================================================ FILE: kintree/config/user/search_api.yaml ================================================ CATEGORY_MATCH_RATIO_LIMIT: 100 CACHE_ENABLED: true CACHE_VALID_DAYS: '7' ================================================ FILE: kintree/database/inventree_api.py ================================================ from ..config import settings import validators from ..common import part_tools from ..common.tools import cprint, download_with_retry from ..config import config_interface import re # Required to use local CA certificates on Linux # For more details, refer to https://github.com/sparkmicro/Ki-nTree/pull/45 import platform import os if platform.system() == 'Linux': cert_path = '/etc/ssl/certs/ca-certificates.crt' if os.path.isfile(cert_path): os.environ['REQUESTS_CA_BUNDLE'] = cert_path # InvenTree from inventree.api import InvenTreeAPI from inventree.company import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak from inventree.part import Part, PartCategory from inventree.currency import CurrencyManager from inventree.stock import StockLocation from inventree.stock import StockItem from inventree.base import ParameterTemplate, Parameter def connect(server: str, username: str, password: str, connect_timeout=5, silent=False, proxies=None, token='') -> bool: ''' Connect to InvenTree server and create API object ''' from wrapt_timeout_decorator import timeout global inventree_api @timeout(dec_timeout=connect_timeout) def get_inventree_api_timeout(): return InvenTreeAPI(server, username=username, password=password, proxies=proxies, token=token) try: inventree_api = get_inventree_api_timeout() except: return False if inventree_api.token: return True return False def set_inventree_db_test_mode(): ''' InvenTree test database setup ''' global inventree_api inventree_api.patch('settings/global/PARAMETER_ENFORCE_UNITS/', {'value': False}) def get_inventree_category_id(category_tree: list) -> int: ''' Get InvenTree category ID from name, specificy parent if subcategory ''' global inventree_api # Fetch all categories part_categories = PartCategory.list(inventree_api, name=category_tree[-1]) if len(part_categories) == 1: return part_categories[0].pk else: if len(category_tree) > 1: # Match the parent category parent_category_id = get_inventree_category_id(category_tree[:-1]) if parent_category_id: for category in part_categories: try: if parent_category_id == category.getParentCategory().pk: return category.pk except AttributeError: pass # # Check parent id match (if passed as argument) # match = True # if parent_category_id: # cprint(f'[TREE]\t{item.getParentCategory().pk} ?= {parent_category_id}', silent=settings.HIDE_DEBUG) # if item.getParentCategory().pk != parent_category_id: # match = False # if match: # cprint(f'[TREE]\t{item.name} ?= {category_name} => True', silent=settings.HIDE_DEBUG) # return item.pk # else: # cprint(f'[TREE]\t{item.name} ?= {category_name} => False', silent=settings.HIDE_DEBUG) return -1 def get_inventree_stock_location_id(stock_location_tree: list) -> int: ''' Get InvenTree stock location ID from name, specificy parent if subcategory ''' global inventree_api # Fetch all categories stock_locations = StockLocation.list(inventree_api, name=stock_location_tree[-1]) if len(stock_locations) == 1: return stock_locations[0].pk else: if len(stock_location_tree) > 1: # Match the parent category parent_stock_location_id = get_inventree_category_id(stock_location_tree[:-1]) if parent_stock_location_id: for location in stock_locations: try: if parent_stock_location_id == location.getParentLocation().pk: return location.pk except AttributeError: pass # # Check parent id match (if passed as argument) # match = True # if parent_stock_location_id: # cprint(f'[TREE]\t{item.getParentCategory().pk} ?= {parent_stock_location_id}', silent=settings.HIDE_DEBUG) # if item.getParentCategory().pk != parent_stock_location_id: # match = False # if match: # cprint(f'[TREE]\t{item.name} ?= {category_name} => True', silent=settings.HIDE_DEBUG) # return item.pk # else: # cprint(f'[TREE]\t{item.name} ?= {category_name} => False', silent=settings.HIDE_DEBUG) return -1 def get_categories() -> dict: '''Fetch InvenTree categories''' global inventree_api categories = {} # Get all categories (list) db_categories = PartCategory.list(inventree_api) def deep_add(tree: dict, keys: list, item: dict): if len(keys) == 1: try: tree[keys[0]].update(item) except (KeyError, AttributeError): tree[keys[0]] = item return return deep_add(tree.get(keys[0]), keys[1:], item) for category in db_categories: parent = category.getParentCategory() children = category.getChildCategories() if not parent and not children: categories[category.name] = None continue elif parent: parent_list = [] while parent: parent_list.insert(0, parent.name) parent = parent.getParentCategory() cat = {category.name: None} deep_add(categories, parent_list, cat) return categories def get_stock_locations() -> dict: '''Fetch InvenTree stock locations''' global inventree_api categories = {} # Get all categories (list) db_categories = StockLocation.list(inventree_api) def deep_add(tree: dict, keys: list, item: dict): if len(keys) == 1: try: tree[keys[0]].update(item) except (KeyError, AttributeError): tree[keys[0]] = item return return deep_add(tree.get(keys[0]), keys[1:], item) for category in db_categories: parent = category.getParentLocation() children = category.getChildLocations() if not parent and not children: categories[category.name] = None continue elif parent: parent_list = [] while parent: parent_list.insert(0, parent.name) parent = parent.getParentLocation() cat = {category.name: None} deep_add(categories, parent_list, cat) return categories def get_category_tree(category_id: int) -> dict: ''' Get all parents of a category''' category = PartCategory(inventree_api, category_id) category_list = {category_id: category.name} while category.parent: category = category.getParentCategory() category_list[category.pk] = category.name return category_list def get_stock_location_tree(id: int) -> dict: ''' Get all parents of a stock_location''' location = StockLocation(inventree_api, id) list = {id: location.name} while location.parent: location = location.getParentLocation() list[location.pk] = location.name return list def create_stock(stock_data: dict) -> dict: return StockItem.create(inventree_api, stock_data) def get_category_parameters(category_id: int) -> list: ''' Get all default parameter templates for category ''' global inventree_api parameter_templates = [] category = PartCategory(inventree_api, category_id) try: category_templates = category.getCategoryParameterTemplates(fetch_parent=True) except AttributeError: category_templates = None if category_templates: for template in category_templates: default_value = template.default_value if not default_value: default_value = '-' parameter_templates.append([template.getTemplate().name, default_value]) return parameter_templates def get_part_info(part_id: int) -> str: ''' Get InvenTree part info from specified Part ID ''' global inventree_api part = Part(inventree_api, part_id) part_info = {'IPN': part.IPN} attachment = part.getAttachments() if attachment: part_info['datasheet'] = f'{inventree_api.base_url.strip("/")}{attachment[0]["attachment"]}' return part_info def set_part_number(part_id: int, ipn: str) -> bool: ''' Set InvenTree part number for specified Part ID ''' data = {'IPN': ipn} update_part(part_id, data) if Part(inventree_api, part_id).IPN == ipn: return True else: return False def get_part_from_ipn(part_ipn='') -> int: ''' Get Part ID from Part IPN ''' global inventree_api parts = Part.list(inventree_api, IPN=part_ipn) if not parts: # No part found return None else: # parts should have only one entry return parts[0] def fetch_part(part_id='', part_ipn='') -> int: ''' Fetch part from database using either ID or IPN ''' from requests.exceptions import HTTPError global inventree_api part = None if part_id: try: part = Part(inventree_api, part_id) except TypeError: # Part ID is invalid (eg. decimal value) cprint('[TREE] Error: Part ID type is invalid') except ValueError: # Part ID is not a positive integer cprint('[TREE] Error: Part ID must be positive') except HTTPError: # Part ID does not exist cprint(f'[TREE] Error: Part with ID={part_id} does not exist in database') elif part_ipn: part = get_part_from_ipn(part_ipn) else: pass return part def is_new_part(category_id: int, part_info: dict) -> int: ''' Check if part exists based on parameters (or description) ''' global inventree_api # Get category object part_category = PartCategory(inventree_api, category_id) # Fetch all parts from category and subcategories part_list = [] part_list.extend(part_category.getParts()) for subcategory in part_category.getChildCategories(): part_list.extend(subcategory.getParts()) # Extract parameter from part info # Verify parameters values are not empty new_part_parameters = part_info['parameters'] if list(set(part_info['parameters'].values())) != ['-'] else None template_list = ParameterTemplate.list(inventree_api) def fetch_template_name(template_id): for item in template_list: if item.pk == template_id: return item.name # Retrieve parent category name for parameters compare try: category_name = part_category.getParentCategory().name except AttributeError: category_name = part_category.name filters = config_interface.load_category_parameters_filters(category=category_name, supplier_config_path=settings.CONFIG_PARAMETERS_FILTERS) # cprint(filters) for part in part_list: # TODO: This statement below seems erroneous... # Compare fields (InvenTree does not allow those to be identicals between two parts) # compare_fields = part_info['name'] == part.name and part_info['revision'] == part.revision # if compare_fields: # cprint(f'[TREE]\tWarning: Found part with same name and revision (pk = {part.pk})', silent=settings.SILENT) # return part.pk # Compare parameters compare_parameters = False # Get part parameters db_part_parameters = part.getParameters() part_parameters = {} for parameter in db_part_parameters: parameter_name = fetch_template_name(parameter.template) parameter_value = parameter.data part_parameters[parameter_name] = parameter_value if new_part_parameters and part_parameters: # Compare database part with new part compare_parameters = part_tools.compare(new_part_parameters=new_part_parameters, db_part_parameters=part_parameters, include_filters=filters) if compare_parameters: cprint(f'[TREE]\tWarning: Found part with same parameters in database (pk = {part.pk})', silent=settings.SILENT) return part.pk # Check if manufacturer part exists in database manufacturer = part_info['manufacturer_name'] mpn = part_info['manufacturer_part_number'] part_pk = is_new_manufacturer_part(manufacturer, mpn, create=False) if part_pk: cprint(f'[TREE]\tWarning: Found part with same manufacturer and MPN in database (pk = {part_pk})', silent=settings.SILENT) return part_pk cprint('\n[TREE]\tNo match found in database', silent=settings.HIDE_DEBUG) return 0 def create_category(parent: str, name: str): ''' Create InvenTree category, use parent for subcategories ''' global inventree_api parent_id = 0 is_new_category = False # Check if category already exists category_list = PartCategory.list(inventree_api) for category in category_list: if name == category.name: try: # Check if parents are the same if category.getParentCategory().name == parent: # Return category ID return category.pk, is_new_category except: return category.pk, is_new_category elif parent == category.name: # Get Parent ID parent_id = category.pk else: pass if parent: if parent_id > 0: category = PartCategory.create(inventree_api, { 'name': name, 'parent': parent_id, }) is_new_category = True else: cprint(f'[TREE]\tError: Check parent category name ({parent})', silent=settings.SILENT) return -1, is_new_category else: # No parent category = PartCategory.create(inventree_api, { 'name': name, 'parent': None, }) is_new_category = True try: category_pk = category.pk except AttributeError: # User does not have the permission to create categories category_pk = 0 return category_pk, is_new_category def upload_part_image(image_url: str, part_id: int, silent=False) -> bool: ''' Upload InvenTree part thumbnail''' global inventree_api # Get image full path image_name = f'{str(part_id)}_thumbnail.jpeg' image_location = settings.search_images + image_name # Download image (multiple attempts) if not download_with_retry(image_url, image_location, filetype='Image', silent=silent): return False # Upload image to InvenTree part = Part(inventree_api, part_id) if part: try: return part.uploadImage(image=image_location) except Exception: return False else: return False def upload_part_datasheet(datasheet_url: str, part_ipn: int, part_pk: int, silent=False) -> str: ''' Upload InvenTree part attachment''' global inventree_api datasheet_name = f'{part_ipn}.pdf' # Get datasheet path based on user settings for local storage if settings.DATASHEET_SAVE_ENABLED: datasheet_location = os.path.join(settings.DATASHEET_SAVE_PATH, datasheet_name) else: datasheet_location = os.path.join(settings.search_datasheets, datasheet_name) if not os.path.isfile(datasheet_location): # Download datasheet (multiple attempts) if not download_with_retry( datasheet_url, datasheet_location, filetype='PDF', timeout=10, silent=silent, ): return '' # Upload Datasheet to InvenTree part = Part(inventree_api, part_pk) if part: try: attachment = part.uploadAttachment(attachment=datasheet_location) return f'{inventree_api.base_url.strip("/")}{attachment["attachment"]}' except Exception: return '' else: return '' def create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None) -> int: ''' Create InvenTree part ''' global inventree_api try: part = Part.create(inventree_api, { 'name': name, 'description': description, 'category': category_id, 'keywords': keywords, 'revision': revision, 'IPN': ipn, 'active': True, 'virtual': False, 'component': True, 'purchaseable': True, }) except Exception as e: cprint('[TREE]\tError: Part creation failed. Check if Ki-nTree settings match InvenTree part settings.', silent=settings.SILENT) cprint(repr(e), silent=settings.SILENT) return 0 if part: return part.pk else: return 0 def set_part_default_location(part_pk: int, location_pk: int): global inventree_api # Retrieve part instance with primary-key of 1 part = Part(inventree_api, pk=part_pk) # Update specified part parameters part.save(data={ "default_location": location_pk, }) def update_part(pk: int, data: dict) -> int: '''Update an existing parts data''' global inventree_api part = Part(inventree_api, pk) if part: part.save(data=data) return part.pk else: return 0 def create_company(company_name: str, manufacturer=False, supplier=False) -> bool: ''' Create InvenTree company ''' global inventree_api if not manufacturer and not supplier: return None company = Company.create(inventree_api, { 'name': company_name, 'description': company_name, 'is_customer': False, 'is_supplier': supplier, 'is_manufacturer': manufacturer, }) return company def get_all_companies() -> dict: ''' Get all existing companies (supplier/manufacturer) from database ''' global inventree_api company_list = Company.list(inventree_api) companies = {} for company in company_list: companies[company.name] = company.pk return companies def get_company_id(company_name: str) -> int: ''' Get company (supplier/manufacturer) primary key (ID) ''' try: return get_all_companies()[company_name] except: return 0 def is_new_manufacturer_part(manufacturer_name: str, manufacturer_mpn: str, create=True) -> int: ''' Check if InvenTree manufacturer part exists to avoid duplicates ''' global inventree_api if not manufacturer_name: return 0 # Fetch all companies cprint('[TREE]\tFetching manufacturers', silent=settings.HIDE_DEBUG) company_list = Company.list(inventree_api, is_manufacturer=True, is_customer=False) companies = {} for company in company_list: companies[company.name] = company try: # Get all parts part_list = companies[manufacturer_name].getManufacturedParts() except: part_list = None if part_list is None: if create: # Create manufacturer cprint(f'[TREE]\tCreating new manufacturer "{manufacturer_name}"', silent=settings.SILENT) create_company( company_name=manufacturer_name, manufacturer=True, ) # Get all parts part_list = [] for item in part_list: try: if manufacturer_mpn in item.MPN: cprint(f'[TREE]\t{item.MPN} ?= {manufacturer_mpn} => True', silent=settings.HIDE_DEBUG) return item.part else: cprint(f'[TREE]\t{item.MPN} ?= {manufacturer_mpn} => False', silent=settings.HIDE_DEBUG) except TypeError: cprint(f'[TREE]\t{item.MPN} ?= {manufacturer_mpn} => *** SKIPPED ***', silent=settings.HIDE_DEBUG) return 0 def is_new_supplier_part(supplier_name: str, supplier_sku: str): ''' Check if InvenTree supplier part exists to avoid duplicates ''' global inventree_api # Fetch all companies cprint('[TREE]\tFetching suppliers', silent=settings.HIDE_DEBUG) company_list = Company.list(inventree_api, is_supplier=True, is_customer=False) companies = {} for company in company_list: companies[company.name] = company try: # Get all parts part_list = companies[supplier_name].getSuppliedParts() except: part_list = None if part_list is None: # Create cprint(f'[TREE]\tCreating new supplier "{supplier_name}"', silent=settings.SILENT) create_company( company_name=supplier_name, supplier=True, ) # Get all parts part_list = [] for item in part_list: if supplier_sku in item.SKU: cprint(f'[TREE]\t{item.SKU} ?= {supplier_sku} => True', silent=settings.HIDE_DEBUG) return False, item else: cprint(f'[TREE]\t{item.SKU} ?= {supplier_sku} => False', silent=settings.HIDE_DEBUG) return True, False def create_manufacturer_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, description: str, datasheet: str) -> bool: ''' Create InvenTree manufacturer part part_id: Part the manufacturer data is linked to manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!) MPN: Manufacture part number datasheet: Datasheet link description: Descriptive notes field ''' global inventree_api # Get Manufacturer ID manufacturer_id = get_company_id(manufacturer_name) if manufacturer_id: # Validate datasheet link if not validators.url(datasheet): datasheet = '' manufacturer_part = ManufacturerPart.create(inventree_api, { 'part': part_id, 'manufacturer': manufacturer_id, 'MPN': manufacturer_mpn, 'link': datasheet, 'description': description, }) if manufacturer_part: return True else: cprint(f'[TREE]\tError: Manufacturer "{manufacturer_name}" not found (failed to create manufacturer part)', silent=settings.SILENT) return False def create_supplier_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, supplier_name: str, supplier_sku: str, description: str, link: str): ''' Create InvenTree supplier part part_id: Part the supplier data is linked to manufacturer_name: Manufacturer the supplier data is linked to manufacturer_mpn: MPN the supplier data is linked to supplier: Company that supplies this SupplierPart object SKU: Stock keeping unit (supplier part number) manufacturer: Company that manufactures the SupplierPart (leave blank if it is the sample as the Supplier!) MPN: Manufacture part number link: Link to part detail page on supplier's website description: Descriptive notes field ''' global inventree_api # Get Supplier ID supplier_id = get_company_id(supplier_name) if not manufacturer_name or not manufacturer_mpn: # Unset manufacturer data manufacturer_name = None manufacturer_mpn = None if supplier_id: # Validate supplier link if not validators.url(link): link = '' supplier_part = SupplierPart.create(inventree_api, { 'part': part_id, 'manufacturer': manufacturer_name, 'MPN': manufacturer_mpn, 'supplier': supplier_id, 'SKU': supplier_sku, 'link': link, 'description': description, }) if supplier_part: return True, supplier_part else: cprint(f'[TREE]\tError: Supplier "{supplier_name}" not found (failed to create supplier part)', silent=settings.SILENT) return False, False def update_price_breaks(supplier_part, price_breaks: dict, currency='USD') -> bool: ''' Update the Price Breaks associated with a supplier part ''' def sanitize_price(price_in): price = re.findall(r'\d+.\d+', price_in)[0] price = price.replace(',', '.') price = price.replace('\xa0', '') return price def convert_currency(price): manager = CurrencyManager(inventree_api) base = manager.getBaseCurrency() if base != currency: try: price = manager.convertCurrency(float(price), currency, base) except Exception: cprint('[TREE]\tWarning: Currency conversion failed.', silent=settings.SILENT) return price if not isinstance(supplier_part, SupplierPart): try: supplier_part = SupplierPart(inventree_api, supplier_part) except: cprint('[TREE]\tWarning: Supplier part not found, skipping price break update', silent=settings.SILENT) return False if not price_breaks: cprint('[TREE]\tWarning: No price breaks found, skipping.', silent=settings.SILENT) return False old_price_breaks = supplier_part.getPriceBreaks() updated = [] # First process existing price breaks for old_price_break in old_price_breaks: quantity = old_price_break.quantity if quantity in price_breaks: price = price_breaks[quantity] # remove everything but the numbers from the price break if isinstance(price, str): price = sanitize_price(price) price = convert_currency(price) old_price_break.save(data={'price': price}) updated.append(quantity) else: old_price_break.delete() for quantity in updated: del price_breaks[quantity] # if any price breaks are left over these will be created for quantity, price in price_breaks.items(): # remove everything but the numbers from the price break if isinstance(price, str): price = sanitize_price(price) price = convert_currency(price) SupplierPriceBreak.create(inventree_api, { 'part': supplier_part.pk, 'quantity': quantity, 'price': price, }) cprint('[INFO]\tSuccess: The price breaks were updated', silent=settings.SILENT) return True def create_parameter_template(name: str, units: str) -> int: ''' Create InvenTree parameter template ''' global inventree_api parameter_templates = ParameterTemplate.list(inventree_api) for item in parameter_templates: if name == item.name: return 0 try: parameter_template = ParameterTemplate.create(inventree_api, { 'name': name, 'units': units if units else '', }) except: cprint(f'[TREE]\tError: Failed to create parameter template "{name}".', silent=settings.SILENT) return 0 if parameter_template: return parameter_template.pk else: return 0 def create_parameter(part_id: int, template_name: int, value: str): ''' Create InvenTree part parameter based on template ''' global inventree_api parameter_template_list = ParameterTemplate.list(inventree_api) template_id = 0 for item in parameter_template_list: if template_name == item.name: template_id = item.pk break # Check if template_id already exists for this part part = Part(inventree_api, part_id) part_parameters = part.getParameters() is_new_part_parameters_template_id = True was_updated = False parameter = None for item in part_parameters: # cprint(f'[TREE]\t{parameter.template} ?= {template_id}', silent=SILENT) if item.template == template_id: is_new_part_parameters_template_id = False if settings.UPDATE_INVENTREE: if value != item.data and value != '-': parameter = item was_updated = True try: parameter.save(data={ 'data': value }) except Exception as e: cprint(f'[TREE]\tError: Failed to update part parameter "{template_name}".', silent=settings.SILENT) if "Could not convert" in e.args[0]['body'].__str__(): cprint(f'[TREE]\tError: Parameter value "{value}" is not allowed by server settings.', silent=settings.SILENT) break # cprint(part_parameters, silent=SILENT) ''' Create parameter only if: - template exists - parameter does not exist for this part ''' if template_id > 0 and is_new_part_parameters_template_id: try: parameter = Parameter.create(inventree_api, { 'model_type': 'part', 'model_id': part.pk, 'template': template_id, 'data': value, }) except Exception as e: cprint(f'[TREE]\tError: Failed to create part parameter "{template_name}".', silent=settings.SILENT) if "Could not convert" in e.args[0]['body'].__str__(): cprint(f'[TREE]\tError: Parameter value "{value}" is not allowed by server settings.', silent=settings.SILENT) if parameter: return parameter.pk, is_new_part_parameters_template_id, was_updated else: if template_id == 0: cprint(f'[TREE]\tError: Parameter template "{template_name}" does not exist', silent=settings.SILENT) return 0, False, False ================================================ FILE: kintree/database/inventree_interface.py ================================================ import copy from ..config import settings from ..common import part_tools, progress from ..common.tools import cprint from ..config import config_interface from ..database import inventree_api from ..search import search_api, automationdirect_api, digikey_api, mouser_api, element14_api, lcsc_api, jameco_api, tme_api category_separator = '/' def connect_to_server(timeout=5) -> bool: ''' Connect to InvenTree server using user settings ''' connect = False settings.load_inventree_settings() if not settings.USERNAME: token = settings.PASSWORD else: token = '' try: connect = inventree_api.connect(server=settings.SERVER_ADDRESS, username=settings.USERNAME, password=settings.PASSWORD, proxies=settings.PROXIES, token=token, connect_timeout=timeout) except TimeoutError: pass if not connect: if not settings.SERVER_ADDRESS: cprint('[TREE]\tError connecting to InvenTree server: missing server address') return connect if not settings.USERNAME: cprint('[TREE]\tError connecting to InvenTree server: missing username') return connect if not settings.PASSWORD: cprint('[TREE]\tError connecting to InvenTree server: missing password') return connect cprint('[TREE]\tError connecting to InvenTree server: invalid address, username or password') else: env = [env_type.name for env_type in settings.Environment if env_type.value == settings.environment][0] cprint(f'[TREE]\tSuccessfully connected to InvenTree server (ENV={env})', silent=settings.SILENT) return connect def category_tree(tree: str) -> str: import re find_prefix = re.match(r'^-+ (.+?)$', tree) if find_prefix: return find_prefix.group(1) return tree def split_category_tree(tree: str) -> list: return category_tree(tree).split(category_separator) def build_category_tree(reload=False, category=None) -> dict: '''Build InvenTree category tree from database data''' category_data = config_interface.load_file(settings.CONFIG_CATEGORIES) def build_tree(tree, left_to_go, level) -> list: try: last_entry = f' {category_tree(tree[-1])}{category_separator}' except IndexError: last_entry = '' if isinstance(left_to_go, dict): for key, value in left_to_go.items(): tree.append(f'{"-" * level}{last_entry}{key}') build_tree(tree, value, level + 1) elif isinstance(left_to_go, list): # Supports legacy structure for item in left_to_go: tree.append(f'{"-" * level}{last_entry}{item}') elif left_to_go is None: pass return if reload: categories = inventree_api.get_categories() category_data.update({'CATEGORIES': categories}) config_interface.dump_file(category_data, settings.CONFIG_CATEGORIES) else: categories = category_data.get('CATEGORIES', {}) # Get specified branch if category: categories = {category: categories.get(category, {})} inventree_categories = [] # Build category tree build_tree(inventree_categories, categories, 0) return inventree_categories def build_stock_location_tree(reload=False, location=None) -> dict: '''Build InvenTree stock locations tree from database data''' locations_data = config_interface.load_file(settings.CONFIG_STOCK_LOCATIONS) def build_tree(tree, left_to_go, level) -> list: try: last_entry = f' {category_tree(tree[-1])}{category_separator}' except IndexError: last_entry = '' if isinstance(left_to_go, dict): for key, value in left_to_go.items(): tree.append(f'{"-" * level}{last_entry}{key}') build_tree(tree, value, level + 1) elif isinstance(left_to_go, list): # Supports legacy structure for item in left_to_go: tree.append(f'{"-" * level}{last_entry}{item}') elif left_to_go is None: pass return if reload: stock_locations = inventree_api.get_stock_locations() locations_data.update({'STOCK_LOCATIONS': stock_locations}) config_interface.dump_file(locations_data, settings.CONFIG_STOCK_LOCATIONS) else: stock_locations = locations_data.get('STOCK_LOCATIONS', {}) # Get specified branch if location: stock_locations = {location: stock_locations.get(location, {})} inventree_stock_locations = [] # Build category tree build_tree(inventree_stock_locations, stock_locations, 0) return inventree_stock_locations def get_categories_from_supplier_data(part_info: dict, supplier_only=False) -> list: ''' Find categories from part supplier data, use "somewhat automatic" matching ''' from thefuzz import fuzz categories = [None, None] try: supplier_category = str(part_info['category_tree'][0]) supplier_subcategory = str(part_info['category_tree'][1]) except KeyError: return categories # Return supplier category, if match not needed if supplier_only: categories[0] = supplier_category categories[1] = supplier_subcategory return categories function_filter = False # TODO: Make 'filter_parameter' user defined? filter_parameter = 'Function Type' # Check existing matches # Load inversed category map category_map = config_interface.load_supplier_categories_inversed(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES) try: for inventree_category in category_map.keys(): for key, inventree_subcategory in category_map[inventree_category].items(): if supplier_subcategory == key: categories[0] = inventree_category # Check if filtering by function if inventree_subcategory.startswith(config_interface.FUNCTION_FILTER_KEY): function_filter = True # Save subcategory if not function filtered if not function_filter: categories[1] = inventree_subcategory break except: pass # Function Filter if not categories[1] and function_filter: cprint(f'[INFO]\tSubcategory is filtered using "{filter_parameter}" parameter', silent=settings.SILENT, end='') # Load parameter map parameter_map = config_interface.load_category_parameters(categories, settings.CONFIG_SUPPLIER_PARAMETERS) # Build compare list compare = [] for supplier_parameter, inventree_parameter in parameter_map.items(): if (supplier_parameter in part_info['parameters'].keys() and inventree_parameter == filter_parameter): compare.append(part_info['parameters'][supplier_parameter]) # Load subcategory map category_map = config_interface.load_supplier_categories(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES)[categories[0]] for inventree_subcategory in category_map.keys(): for item in compare: fuzzy_match = fuzz.partial_ratio(inventree_subcategory, item) display_result = f'"{inventree_subcategory}" ?= "{item}"'.ljust(50) cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG) if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT: categories[1] = inventree_subcategory.replace(config_interface.FUNCTION_FILTER_KEY, '') break if categories[1]: cprint('\t[ PASS ]', silent=settings.SILENT) break if not categories[1] and function_filter: cprint('\t[ FAILED ]', silent=settings.SILENT) # Automatic Match if not (categories[0] and categories[1]): # Load category map category_map = config_interface.load_supplier_categories(supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES) def find_supplier_category_match(supplier_category: str, ignore_categories=False): # Check for match with Inventree categories category_match = None subcategory_match = None for inventree_category in category_map.keys(): fuzzy_match = 0 if not ignore_categories: fuzzy_match = fuzz.partial_ratio(supplier_category, inventree_category) display_result = f'"{supplier_category}" ?= "{inventree_category}"'.ljust(50) cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG) if fuzzy_match < settings.CATEGORY_MATCH_RATIO_LIMIT and category_map[inventree_category]: # Compare to subcategories for inventree_subcategory in category_map[inventree_category]: fuzzy_match = fuzz.partial_ratio(supplier_category, inventree_subcategory) display_result = f'"{supplier_category}" ?= "{inventree_subcategory}"'.ljust(50) cprint(f'{display_result} => {fuzzy_match}', silent=settings.HIDE_DEBUG) if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT: subcategory_match = inventree_subcategory break if fuzzy_match >= settings.CATEGORY_MATCH_RATIO_LIMIT: category_match = inventree_category break return category_match, subcategory_match # Find category and subcategories match category, subcategory = find_supplier_category_match(supplier_category) if category: categories[0] = category if subcategory: categories[1] = subcategory # Run match with supplier subcategory if not categories[0] or not categories[1]: if categories[0]: # If category was found: ignore them for the comparison category, subcategory = find_supplier_category_match(supplier_subcategory, ignore_categories=True) else: category, subcategory = find_supplier_category_match(supplier_subcategory) if category and not categories[0]: categories[0] = category if subcategory and not categories[1]: categories[1] = subcategory # Final checks if not categories[0]: cprint(f'[INFO]\tWarning: "{part_info["category_tree"][0]}" did not match any supplier category ', silent=settings.SILENT) else: cprint(f'[INFO]\tCategory: "{categories[0]}"', silent=settings.SILENT) if not categories[1]: cprint(f'[INFO]\tWarning: "{part_info["category_tree"][1]}" did not match any supplier subcategory ', silent=settings.SILENT) else: cprint(f'[INFO]\tSubcategory: "{categories[1]}"', silent=settings.SILENT) # print(f'{supplier_category=} | {supplier_subcategory=} | {categories[0]=} | {categories[1]=}') return categories def translate_form_to_inventree(part_info: dict, category_tree: list, is_custom=False) -> dict: ''' Using supplier part data and categories, fill-in InvenTree part dictionary ''' # Copy template inventree_part = copy.deepcopy(settings.inventree_part_template) # Translate form data to inventree part inventree_part['category_tree'] = category_tree inventree_part['name'] = part_info['name'] inventree_part['description'] = part_info['description'] inventree_part['revision'] = part_info['revision'] inventree_part['keywords'] = part_info['keywords'] inventree_part['supplier_name'] = part_info['supplier_name'] inventree_part['supplier_part_number'] = part_info['supplier_part_number'] inventree_part['manufacturer_name'] = part_info['manufacturer_name'] inventree_part['manufacturer_part_number'] = part_info['manufacturer_part_number'] inventree_part['IPN'] = part_info.get('IPN', '') # Replace whitespaces in URL inventree_part['supplier_link'] = part_info['supplier_link'].replace(' ', '%20') inventree_part['datasheet'] = part_info['datasheet'].replace(' ', '%20') # Image URL is not shown to user so force default key/value try: inventree_part['image'] = part_info['image'].replace(' ', '%20') except AttributeError: # Part image URL is null (no product picture) pass inventree_part['pricing'] = part_info.get('pricing', {}) inventree_part['currency'] = part_info.get('currency', 'USD') parameters = part_info.get('parameters', {}) # Load parameters map if category_tree: parameter_map = config_interface.load_category_parameters( categories=category_tree, supplier_config_path=settings.CONFIG_SUPPLIER_PARAMETERS, ) else: cprint('[INFO]\tWarning: Parameter map not loaded (no category selected)', silent=settings.SILENT) if not is_custom: # Add Parameters if parameter_map: parameters_missing = [] for supplier_param, inventree_param in parameter_map.items(): # Some parameters may not be mapped if inventree_param not in inventree_part['parameters'].keys(): if supplier_param == 'Manufacturer Part Number': inventree_part['parameters'][inventree_param] = part_info['manufacturer_part_number'] elif inventree_param == 'image': inventree_part['existing_image'] = supplier_param else: try: parameter_value = part_tools.clean_parameter_value( category=category_tree[0], name=supplier_param, value=parameters[supplier_param], ) inventree_part['parameters'][inventree_param] = parameter_value except KeyError: parameters_missing.append(supplier_param) if parameters_missing: msg = '[INFO]\tWarning: The following parameters were not found in supplier data:\n' msg += str(parameters_missing) cprint(msg, silent=settings.SILENT) # Check for missing InvenTree parameters and fill value with dash for inventree_param in parameter_map.values(): if inventree_param == 'image': continue if inventree_param not in inventree_part['parameters'].keys(): inventree_part['parameters'][inventree_param] = '-' # Check for extra parameters which weren't mapped parameters_unmapped = [] for search_param in parameters.keys(): if search_param not in parameter_map.keys(): parameters_unmapped.append(search_param) if parameters_unmapped: if not settings.SILENT: msg = f'[INFO]\tThe following parameters are not mapped in {inventree_part["supplier_name"]} parameters configuration:\n' msg += str(parameters_unmapped) print(msg) else: cprint(f'[INFO]\tWarning: Parameter map for "{category_tree[0]}" does not exist or is empty', silent=settings.SILENT) return inventree_part def get_supplier_name(supplier: str) -> str: ''' Get InvenTree supplier name ''' supplier_name = supplier for supplier, data in settings.CONFIG_SUPPLIERS.items(): if data['name'] == supplier_name: # Update supplier name supplier_name = supplier break return supplier_name def translate_supplier_to_form(supplier: str, part_info: dict) -> dict: ''' Translate supplier data to user form format ''' part_form = {} def get_value_from_user_key(user_key: str, default_key: str, default_value=None) -> str: ''' Get value mapped from user search key, else default search key ''' user_search_key = None if supplier == 'Digi-Key': user_search_key = settings.CONFIG_DIGIKEY.get(user_key, None) elif supplier == 'Mouser': user_search_key = settings.CONFIG_MOUSER.get(user_key, None) elif supplier in ['Farnell', 'Newark', 'Element14']: user_search_key = settings.CONFIG_ELEMENT14.get(user_key, None) elif supplier == 'LCSC': user_search_key = settings.CONFIG_LCSC.get(user_key, None) elif supplier == 'Jameco': user_search_key = settings.CONFIG_JAMECO.get(user_key, None) elif supplier == 'TME': user_search_key = settings.CONFIG_TME.get(user_key, None) elif supplier == 'AutomationDirect': user_search_key = settings.CONFIG_AUTOMATIONDIRECT.get(user_key, None) else: return default_value # If no user key, use default if not user_search_key: return part_info.get(default_key, default_value) # Get value for user key, return value from default key if not found return part_info.get(user_search_key, part_info.get(default_key, default_value)) # Check that supplier argument is valid if not supplier and supplier != 'custom': return part_form # Get default keys if supplier == 'Digi-Key': default_search_keys = digikey_api.get_default_search_keys() elif supplier == 'Mouser': default_search_keys = mouser_api.get_default_search_keys() elif supplier in ['Farnell', 'Newark', 'Element14']: default_search_keys = element14_api.get_default_search_keys() elif supplier == 'LCSC': default_search_keys = lcsc_api.get_default_search_keys() elif supplier == 'Jameco': default_search_keys = jameco_api.get_default_search_keys() elif supplier == 'TME': default_search_keys = tme_api.get_default_search_keys() elif supplier == 'AutomationDirect': default_search_keys = automationdirect_api.get_default_search_keys() else: # Empty array of default search keys default_search_keys = [''] * len(digikey_api.get_default_search_keys()) # Default revision revision = settings.CONFIG_IPN.get('INVENTREE_DEFAULT_REV', '') # Translate supplier data to form fields part_form['name'] = get_value_from_user_key('SEARCH_NAME', default_search_keys[0], default_value='') part_form['description'] = get_value_from_user_key('SEARCH_DESCRIPTION', default_search_keys[1], default_value='') part_form['revision'] = get_value_from_user_key('SEARCH_REVISION', default_search_keys[2], default_value=revision) part_form['keywords'] = get_value_from_user_key('SEARCH_KEYWORDS', default_search_keys[3], default_value='') part_form['supplier_name'] = settings.CONFIG_SUPPLIERS[supplier]['name'] part_form['supplier_part_number'] = get_value_from_user_key('SEARCH_SKU', default_search_keys[4], default_value='') part_form['supplier_link'] = get_value_from_user_key('SEARCH_SUPPLIER_URL', default_search_keys[7], default_value='') part_form['manufacturer_name'] = get_value_from_user_key('SEARCH_MANUFACTURER', default_search_keys[5], default_value='') part_form['manufacturer_part_number'] = get_value_from_user_key('SEARCH_MPN', default_search_keys[6], default_value='') part_form['datasheet'] = get_value_from_user_key('SEARCH_DATASHEET', default_search_keys[8], default_value='') part_form['image'] = get_value_from_user_key('', default_search_keys[9], default_value='') return part_form def supplier_search(supplier: str, part_number: str, test_mode=False) -> dict: ''' Wrapper for supplier search, allow use of cached data (limited daily API calls) ''' part_info = {} # Check part number exist if not part_number: cprint('\n[MAIN]\tError: Missing Part Number', silent=settings.SILENT) return part_info store = '' if supplier in ['Farnell', 'Newark', 'Element14']: try: element14_config = config_interface.load_file(settings.CONFIG_ELEMENT14_API) store = element14_config.get(f'{supplier.upper()}_STORE', '').replace(' ', '') except AttributeError: cprint(f'\n[INFO]\tWarning: {supplier.upper()}_STORE value not found', silent=False) search_filename = f"{settings.search_results['directory']}{supplier}{store}_{part_number}{settings.search_results['extension']}" # Get cached data, if cache is enabled (else returns None) part_cache = search_api.load_from_file(search_filename, test_mode) if part_cache: cprint(f'\n[MAIN]\tUsing {supplier} cached data for {part_number}', silent=settings.SILENT) part_info = part_cache else: cprint(f'\n[MAIN]\t{supplier} search for {part_number}', silent=settings.SILENT) if supplier == 'Digi-Key': part_info = digikey_api.fetch_part_info(part_number) elif supplier == 'Mouser': part_info = mouser_api.fetch_part_info(part_number) elif supplier in ['Farnell', 'Newark', 'Element14']: part_info = element14_api.fetch_part_info(part_number, supplier) elif supplier == 'LCSC': part_info = lcsc_api.fetch_part_info(part_number) elif supplier == 'Jameco': part_info = jameco_api.fetch_part_info(part_number) elif supplier == 'TME': part_info = tme_api.fetch_part_info(part_number) elif supplier == 'AutomationDirect': part_info = automationdirect_api.fetch_part_info(part_number) # Check supplier data exist if not part_info: cprint(f'[INFO]\tError: Failed to fetch data for "{part_number}"', silent=settings.SILENT) # Save search results if part_info: update_ts = not bool(part_cache) or test_mode search_api.save_to_file(part_info, search_filename, update_ts=update_ts) return part_info def inventree_fuzzy_company_match(name: str) -> str: ''' Fuzzy match company name to exisiting companies ''' from thefuzz import fuzz inventree_companies = inventree_api.get_all_companies() for company_name in inventree_companies.keys(): cprint(f'{name.lower()} == {company_name.lower()} % {fuzz.partial_ratio(name.lower(), company_name.lower())}', silent=settings.HIDE_DEBUG) if fuzz.partial_ratio(name.lower(), company_name.lower()) == 100 and len(name) == len(company_name): return company_name return name def inventree_create_manufacturer_part(part_id: int, manufacturer_name: str, manufacturer_mpn: str, datasheet: str, description: str) -> bool: ''' Create manufacturer part ''' cprint('\n[MAIN]\tCreating manufacturer part', silent=settings.SILENT) manufacturer_part = inventree_api.is_new_manufacturer_part(manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn) if manufacturer_part: cprint('[INFO]\tManufacturer part already exists, skipping.', silent=settings.SILENT) else: # Create a new manufacturer part is_manufacturer_part_created = inventree_api.create_manufacturer_part(part_id=part_id, manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, datasheet=datasheet, description=description) if is_manufacturer_part_created: cprint('[INFO]\tSuccess: Added new manufacturer part', silent=settings.SILENT) return True return False def inventree_create_supplier_part(part) -> bool: return def get_inventree_stock_location_id(stock_location_tree: list): return inventree_api.get_inventree_stock_location_id(stock_location_tree) def inventree_create(part_info: dict, stock=None, kicad=False, symbol=None, footprint=None, show_progress=True, is_custom=False, enable_upload=True): ''' Create InvenTree part from supplier part data and categories ''' part_pk = 0 new_part = False category_tree = part_info['category_tree'] if not category_tree: cprint(f'[INFO]\tError: Category tree is empty {category_tree=}', silent=settings.SILENT) return new_part, part_pk, {} # Translate to InvenTree part format inventree_part = translate_form_to_inventree( part_info=part_info, category_tree=category_tree, is_custom=is_custom, ) if not inventree_part: cprint('\n[MAIN]\tError: Failed to process form data', silent=settings.SILENT) category_pk = inventree_api.get_inventree_category_id(category_tree) if category_pk <= 0: cprint(f'[ERROR]\tCategory ({category_tree}) does not exist in InvenTree', silent=settings.SILENT) else: if settings.CHECK_EXISTING: # Check if part already exists part_pk = inventree_api.is_new_part(category_pk, inventree_part) # Part exists if part_pk > 0: cprint('[INFO]\tPart already exists, skipping.', silent=settings.SILENT) info = inventree_api.get_part_info(part_pk) if info: # Update InvenTree part number inventree_part = {**inventree_part, **info} # Update InvenTree URL inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/' else: inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/' # Part is new if not part_pk: new_part = True if settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', True): # Generate Placeholder Internal Part Number ipn = part_tools.generate_part_number( category=category_tree[0], part_pk=0, category_code=part_info.get('category_code', ''), ) else: ipn = '' # Create a new Part # Use the pk (primary-key) of the category part_pk = inventree_api.create_part( category_id=category_pk, name=inventree_part['name'], description=inventree_part['description'], revision=inventree_part['revision'], keywords=inventree_part['keywords'], ipn=ipn) # Check part primary key if not part_pk: return new_part, part_pk, inventree_part # Progress Update if not progress.update_progress_bar(show_progress): return new_part, part_pk, inventree_part if settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', True): # Generate Internal Part Number cprint('\n[MAIN]\tGenerating Internal Part Number', silent=settings.SILENT) if settings.CONFIG_IPN.get('IPN_USE_MANUFACTURER_PART_NUMBER', False): ipn = inventree_part['manufacturer_part_number'] else: ipn = part_tools.generate_part_number( category=category_tree[0], part_pk=part_pk, category_code=part_info.get('category_code', ''), ) cprint(f'[INFO]\tInternal Part Number = {ipn}', silent=settings.SILENT) # Update InvenTree part number ipn_update = inventree_api.set_part_number(part_pk, ipn) if not ipn_update: cprint('\n[INFO]\tError updating IPN', silent=settings.SILENT) inventree_part['IPN'] = ipn # Update InvenTree URL inventree_part['inventree_url'] = f'{settings.PART_URL_ROOT}{part_pk}/' # Progress Update if not progress.update_progress_bar(show_progress): return new_part, part_pk, inventree_part if part_pk > 0: if new_part: cprint('[INFO]\tSuccess: Added new part to InvenTree', silent=settings.SILENT) if inventree_part.get('existing_image', ''): inventree_api.update_part( part_pk, data={'existing_image': inventree_part['existing_image']}) elif inventree_part['image']: if enable_upload: # Add image image_result = inventree_api.upload_part_image(inventree_part['image'], part_pk, silent=settings.SILENT) if not image_result: cprint('[TREE]\tWarning: Failed to upload part image', silent=settings.SILENT) if inventree_part['datasheet'] and settings.DATASHEET_UPLOAD: if enable_upload: # Upload datasheet datasheet_link = inventree_api.upload_part_datasheet( datasheet_url=inventree_part['datasheet'], part_ipn=inventree_part['IPN'], part_pk=part_pk, silent=settings.SILENT, ) if not datasheet_link: cprint('[TREE]\tWarning: Failed to upload part datasheet', silent=settings.SILENT) else: cprint('[TREE]\tSuccess: Uploaded part datasheet', silent=settings.SILENT) if kicad: try: symbol_name = ipn except UnboundLocalError: symbol_name = inventree_part.get('manufacturer_part_number') # Create symbol & footprint parameters if symbol: symbol = f'{symbol.split(":")[0]}:{symbol_name}' inventree_part['parameters']['Symbol'] = symbol if footprint: inventree_part['parameters']['Footprint'] = footprint if not inventree_part['parameters']: category_parameters = inventree_api.get_category_parameters(category_pk) # Add category-defined parameters for parameter in category_parameters: inventree_part['parameters'][parameter[0]] = parameter[1] # Create parameters if len(inventree_part['parameters']) > 0: if not inventree_process_parameters( part_id=part_pk, parameters=inventree_part['parameters'], show_progress=show_progress): return new_part, part_pk, inventree_part # Create manufacturer part if inventree_part['manufacturer_name'] and inventree_part['manufacturer_part_number']: # Overwrite manufacturer name with matching one from database manufacturer_name = inventree_fuzzy_company_match(inventree_part['manufacturer_name']) # Get MPN manufacturer_mpn = inventree_part['manufacturer_part_number'] cprint('\n[MAIN]\tCreating manufacturer part', silent=settings.SILENT) manufacturer_part = inventree_api.is_new_manufacturer_part( manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, ) if manufacturer_part: cprint('[INFO]\tManufacturer part already exists, skipping.', silent=settings.SILENT) else: # Create a new manufacturer part is_manufacturer_part_created = inventree_api.create_manufacturer_part( part_id=part_pk, manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, datasheet=inventree_part['datasheet'], description=inventree_part['description'], ) if is_manufacturer_part_created: cprint('[INFO]\tSuccess: Added new manufacturer part', silent=settings.SILENT) # Create supplier part if inventree_part['supplier_name'] and inventree_part['supplier_part_number']: # Overwrite manufacturer name with matching one from database supplier_name = inventree_fuzzy_company_match(inventree_part['supplier_name']) # Get SKU supplier_sku = inventree_part['supplier_part_number'] cprint('\n[MAIN]\tCreating supplier part', silent=settings.SILENT) is_new_supplier_part, supplier_part = inventree_api.is_new_supplier_part( supplier_name=supplier_name, supplier_sku=supplier_sku) if not is_new_supplier_part: cprint('[INFO]\tSupplier part already exists, skipping.', silent=settings.SILENT) else: # Create a new supplier part is_supplier_part_created, supplier_part = inventree_api.create_supplier_part( part_id=part_pk, manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, supplier_name=supplier_name, supplier_sku=supplier_sku, description=inventree_part['description'], link=inventree_part['supplier_link'], ) if is_supplier_part_created: cprint('[INFO]\tSuccess: Added new supplier part', silent=settings.SILENT) if supplier_part and settings.PRICING_UPLOAD: cprint('\n[MAIN]\tProcessing Price Breaks', silent=settings.SILENT) inventree_api.update_price_breaks( supplier_part=supplier_part, price_breaks=inventree_part['pricing'], currency=inventree_part['currency']) if stock is not None: stock['part'] = part_pk inventree_api.create_stock(stock) if stock['make_default']: inventree_api.set_part_default_location(part_pk, stock['location']) # Progress Update if not progress.update_progress_bar(show_progress): pass return new_part, part_pk, inventree_part def inventree_process_parameters(part_id: str, parameters: dict, show_progress=True) -> bool: ''' Create or Update parameters for an InvenTree part''' cprint('\n[MAIN]\tCreating parameters', silent=settings.SILENT) parameters_lists = [ [], # Store new parameters [], # Store updated parameters [], # Store unchanged parameters ] for name, value in parameters.items(): parameter, is_new_parameter, was_updated = inventree_api.create_parameter(part_id=part_id, template_name=name, value=value) # Progress Update if not progress.update_progress_bar(show_progress, increment=0.03): return False if is_new_parameter: parameters_lists[0].append(name) elif was_updated: parameters_lists[1].append(name) else: parameters_lists[2].append(name) if parameters_lists[0]: cprint('[INFO]\tSuccess: The following parameters were created:', silent=settings.SILENT) for item in parameters_lists[0]: cprint(f'--->\t{item}', silent=settings.SILENT) if parameters_lists[1]: cprint('[INFO]\tSuccess: The following parameters were updated:', silent=settings.SILENT) for item in parameters_lists[1]: cprint(f'--->\t{item}', silent=settings.SILENT) if parameters_lists[2]: cprint('[TREE]\tWarning: The following parameters were skipped:', silent=settings.SILENT) for item in parameters_lists[2]: cprint(f'--->\t{item}', silent=settings.SILENT) return True def inventree_create_alternate(part_info: dict, part_id='', part_ipn='', show_progress=None) -> bool: ''' Create alternate manufacturer and supplier entries for an existing InvenTree part ''' result = False cprint('\n[MAIN]\tSearching for original part in database', silent=settings.SILENT) part = inventree_api.fetch_part(part_id, part_ipn) if part: part_pk = part.pk part_description = part.description cprint(f'[INFO] Success: Found original part in database (ID = {part_pk} | Description = "{part_description}")', silent=settings.SILENT) else: cprint('[INFO] Error: Original part was not found in database', silent=settings.SILENT) return result # Translate to InvenTree part format category_tree = inventree_api.get_category_tree(part.category) category_tree = list(category_tree.values()) category_tree.reverse() inventree_part = translate_form_to_inventree( part_info=part_info, category_tree=category_tree, ) # If the part has no image yet try to upload it from the data if not part.image: image = part_info.get('image', '') existing_image = inventree_part.get('existing_image', '') if existing_image: inventree_api.update_part(pk=part_pk, data={'existing_image': existing_image}) elif image: inventree_api.upload_part_image(image_url=image, part_id=part_pk, silent=settings.SILENT) # create or update parameters if inventree_part.get('parameters', {}): inventree_process_parameters(part_id=part_pk, parameters=inventree_part['parameters'], show_progress=show_progress) # Overwrite manufacturer name with matching one from database manufacturer_name = inventree_fuzzy_company_match(part_info.get('manufacturer_name', '')) manufacturer_mpn = part_info.get('manufacturer_part_number', '') datasheet = part_info.get('datasheet', '') attachment = part.getAttachments() # if datasheet upload is enabled and no attachment present yet then upload if settings.DATASHEET_UPLOAD and not attachment: if datasheet: part_info['datasheet'] = inventree_api.upload_part_datasheet( datasheet_url=datasheet, part_ipn=part_ipn, part_pk=part_id, silent=settings.SILENT, ) if not part_info['datasheet']: cprint('[TREE]\tWarning: Failed to upload part datasheet', silent=settings.SILENT) else: cprint('[TREE]\tSuccess: Uploaded part datasheet', silent=settings.SILENT) # if an attachment is present, set it as the datasheet field if attachment: part_info['datasheet'] = f'{inventree_api.inventree_api.base_url.strip("/")}{attachment[0]["attachment"]}' # Create manufacturer part if manufacturer_name and manufacturer_mpn: inventree_create_manufacturer_part(part_id=part_pk, manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, datasheet=datasheet, description=part_description) else: cprint('[INFO]\tWarning: No manufacturer part to create', silent=settings.SILENT) # Progress Update if not progress.update_progress_bar(show_progress, increment=0.2): return supplier_name = part_info.get('supplier_name', '') supplier_sku = part_info.get('supplier_part_number', '') supplier_link = part_info.get('supplier_link', '') # Add supplier alternate if supplier_name and supplier_sku: cprint('\n[MAIN]\tCreating supplier part', silent=settings.SILENT) is_new_supplier_part, supplier_part = inventree_api.is_new_supplier_part( supplier_name=supplier_name, supplier_sku=supplier_sku) if not is_new_supplier_part: cprint('[INFO]\tSupplier part already exists, skipping.', silent=settings.SILENT) else: # Create a new supplier part is_supplier_part_created, supplier_part = inventree_api.create_supplier_part( part_id=part_pk, manufacturer_name=manufacturer_name, manufacturer_mpn=manufacturer_mpn, supplier_name=supplier_name, supplier_sku=supplier_sku, description=part_description, link=supplier_link) if is_supplier_part_created: cprint('[INFO]\tSuccess: Added new supplier part', silent=settings.SILENT) result = True if supplier_part and settings.PRICING_UPLOAD: cprint('\n[MAIN]\tProcessing Price Breaks', silent=settings.SILENT) inventree_api.update_price_breaks( supplier_part=supplier_part, price_breaks=inventree_part['pricing'], currency=inventree_part['currency']) result = True else: cprint('[INFO]\tWarning: No supplier part to create', silent=settings.SILENT) return result ================================================ FILE: kintree/gui/gui.py ================================================ import os import flet as ft from ..config import settings from .views.common import update_theme, handle_transition from .views.main import ( PartSearchView, InventreeView, KicadView, CreateView, ) from .views.settings import ( UserSettingsView, SupplierSettingsView, InvenTreeSettingsView, KiCadSettingsView, ) def init_gui(page: ft.Page): '''Initialize page''' # Alignments page.horizontal_alignment = ft.CrossAxisAlignment.CENTER page.vertical_alignment = ft.MainAxisAlignment.CENTER page.scroll = ft.ScrollMode.ALWAYS # Window Icon page.window.icon = os.path.join(settings.PROJECT_DIR, 'gui', 'logo.ico') page.window.title_bar_hidden = True # Theme update_theme(page) # Creating a progress bar that will be used # to show the user that the app is busy doing something page.splash = ft.ProgressBar(visible=False) # Update page.update() def kintree_gui(page: ft.Page): '''Ki-nTree GUI''' # Init init_gui(page) # Create main views part_view = PartSearchView(page) inventree_view = InventreeView(page) kicad_view = KicadView(page) create_view = CreateView(page) # Create settings views user_settings_view = UserSettingsView(page) supplier_settings_view = SupplierSettingsView(page) inventree_settings_view = InvenTreeSettingsView(page) kicad_settings_view = KiCadSettingsView(page) # Routing def route_change(route): # print(f'\n--> Routing to {route.route}') if '/main' in page.route or page.route == '/': page.views.clear() if 'part' in page.route or page.route == '/': page.views.append(part_view) if 'inventree' in page.route: page.views.append(inventree_view) elif 'kicad' in page.route: page.views.append(kicad_view) elif 'create' in page.route: page.views.append(create_view) elif '/settings' in page.route: if '/settings' in page.views[-1].route: page.views.pop() if 'user' in page.route: page.views.append(user_settings_view) elif 'supplier' in page.route: page.views.append(supplier_settings_view) elif 'inventree' in page.route: page.views.append(inventree_settings_view) elif 'kicad' in page.route: page.views.append(kicad_settings_view) else: page.views.append(user_settings_view) page.update() def view_pop(view): '''Pop setting view''' page.views.pop() top_view = page.views[-1] if 'main' in top_view.route: handle_transition(page, transition=True) # Route and render page.go(top_view.route) if 'main' in top_view.route: handle_transition( page, transition=False, update_page=True, timeout=0.3, ) if '/main/part' in top_view.route or '/main/inventree' in top_view.route: top_view.partial_update() page.on_route_change = route_change page.on_view_pop = view_pop page.go(page.route) ================================================ FILE: kintree/gui/views/common.py ================================================ from enum import Enum from typing import Optional, List import flet as ft GUI_PARAMS = { 'nav_rail_min_width': 100, 'nav_rail_width': 400, 'nav_rail_alignment': -0.9, 'nav_rail_icon_size': 40, 'nav_rail_text_size': 16, 'nav_rail_padding': 10, 'textfield_width': 600, 'textfield_dense': True, 'textfield_space_after': 3, 'dropdown_width': 600, 'dropdown_dense': False, 'searchfield_width': 300, 'button_width': 110, 'button_height': 56, 'icon_size': 40, 'text_size': 16, } # Contains data from all views data_from_views = {} class DialogType(Enum): VALID = 'valid' WARNING = 'warning' ERROR = 'error' def handle_transition(page: ft.Page, transition: bool, update_page=False, timeout=0): # print(f'{transition=} | {update_page=} | {timeout=}') if transition: transition = ft.PageTransitionTheme.CUPERTINO page.theme.page_transitions.android = transition page.theme.page_transitions.ios = transition page.theme.page_transitions.linux = transition page.theme.page_transitions.macos = transition page.theme.page_transitions.windows = transition else: page.theme.page_transitions.android = ft.PageTransitionTheme.NONE page.theme.page_transitions.ios = ft.PageTransitionTheme.NONE page.theme.page_transitions.linux = ft.PageTransitionTheme.NONE page.theme.page_transitions.macos = ft.PageTransitionTheme.NONE page.theme.page_transitions.windows = ft.PageTransitionTheme.NONE # Wait if timeout: import time time.sleep(timeout) # Update if update_page: page.update() def update_theme(page: ft.Page, mode='light', transition=False, compact=True): # Color theme page.theme_mode = mode # UI theme theme = ft.Theme() page.theme = theme # Make it more compact if compact: page.theme.visual_density = ft.ThemeVisualDensity.COMPACT else: page.theme.visual_density = ft.ThemeVisualDensity.STANDARD # Disable transitions by default handle_transition(page, transition=False) class CommonView(ft.View): '''Common view to all GUI views''' _page = None navigation_rail = None title = None column = None fields = None data = None dialog = None def __init__(self, page: ft.Page, appbar: ft.AppBar, navigation_rail: ft.NavigationRail): # Store page pointer self._page = page # Init view super().__init__(route=self.route, appbar=appbar) # Set navigation rail if not self.navigation_rail: self.navigation_rail = navigation_rail def build_column(self): # Empty column (to be set inside the children views) self.column = ft.Column() def build(self): # Build column if not self.column: self.build_column() # Set view controls self.controls = [ ft.Row( controls=[ self.navigation_rail, ft.VerticalDivider(width=1), self.column, ], expand=True, ), ] def build_dialog(self): return None def build_snackbar(self, d_type: DialogType, message: str): if d_type == DialogType.VALID: self.dialog = ft.SnackBar( bgcolor=ft.colors.GREEN_100, content=ft.Text( message, color=ft.colors.GREEN_700, size=GUI_PARAMS['nav_rail_text_size'], weight=ft.FontWeight.BOLD, ), ) elif d_type == DialogType.WARNING: self.dialog = ft.SnackBar( bgcolor=ft.colors.AMBER_100, content=ft.Text( message, color=ft.colors.AMBER_800, size=GUI_PARAMS['nav_rail_text_size'], weight=ft.FontWeight.BOLD, ), ) elif d_type == DialogType.ERROR: self.dialog = ft.SnackBar( bgcolor=ft.colors.RED_100, content=ft.Text( message, color=ft.colors.RED_700, size=GUI_PARAMS['nav_rail_text_size'], weight=ft.FontWeight.BOLD, ), ) def show_dialog( self, d_type: Optional[DialogType] = None, message: Optional[str] = None, snackbar=True, open=True, ): if snackbar: self.build_snackbar(d_type, message) if isinstance(self.dialog, ft.SnackBar): self._page.snack_bar = self.dialog self._page.snack_bar.open = True elif isinstance(self.dialog, ft.Banner): self._page.banner = self.dialog self._page.banner.open = open elif isinstance(self.dialog, ft.AlertDialog): if open: self._page.open(self.dialog) else: self._page.close(self.dialog) self._page.update() class SwitchWithRefs(ft.Switch): '''Link the visibility of other fields to a switch value''' linked_refs = [] def __init__( self, refs: List[ft.Ref] = None, reverse_dir: bool = False, **kwargs, ): super().__init__(**kwargs) if refs: self.refs = refs self.enable_refs(self.value) self.reverse_dir = reverse_dir def enable_refs(self, enable): if self.reverse_dir: enable = not enable for ref in self.linked_refs: ref.current.visible = enable try: ref.current.update() except AssertionError: # Control not added to page yet pass def process_change(self, e, handler, *args, **kwargs): enable = False if e.data == 'true': enable = True self.enable_refs(enable) handler(e, *args, **kwargs) @property def refs(self): return self.linked_refs @refs.setter def refs(self, references: List[ft.Ref]): if references: self.linked_refs = [] for ref in references: try: if ref.current is None: raise Exception(f'Reference "{ref.current}" needs to be added to the page first') except AttributeError: raise Exception(f'"{ref}" is not a Flet Ref (type: {type(ref)})') # if ft.Control not in ref.current.__class__.__mro__: # raise Exception(f'"{ref.current}" is not a Flet Control ({type(ref.current)})') self.linked_refs.append(ref) if self.linked_refs: self.enable_refs(self.value) @ft.Switch.on_change.setter def on_change(self, handler, *args, **kwargs): ft.Switch.on_change.fset( self, lambda e: self.process_change(e, handler, *args, **kwargs) ) class DropdownWithSearch(ft.UserControl): '''Implements a dropdown with search box''' dropdown = None search_button = None search_field = None search_box = None search_width = None def build(self): return ft.Row([ self.dropdown, self.search_box, self.search_button, ]) def __str__(self): return f'dropdown_with_search {{dropdown: {self.dropdown}, search_field: {self.search_field}}}' def __init__( self, label: Optional[str] = None, dr_width: Optional[int] = None, sr_width: Optional[int] = None, dense: Optional[bool] = None, disabled=False, sr_animate=100, options=None, on_change=None, **kwargs, ): super().__init__(**kwargs) self._options = options self.dropdown = ft.Dropdown( label=label, width=dr_width, dense=dense, options=options, on_change=on_change, ) self.search_button = ft.IconButton( 'search', on_click=self.search_now ) self.search_field = ft.TextField( border="none", width=sr_width, dense=dense, on_change=self.on_search, ) self.search_box = ft.Container( content=self.search_field, width=0, animate=ft.Animation(sr_animate), ) self.disabled = disabled self.search_width = sr_width @property def label(self): return self.dropdown.label @label.setter def label(self, label): self.dropdown.label = label @property def value(self): return self.dropdown.value @value.setter def value(self, value): self.dropdown.value = value if value is None: self.search_field.value = None self.done_search() @property def disabled(self): return self.dropdown.disabled @disabled.setter def disabled(self, disabled): try: self.dropdown.disabled = disabled self.dropdown.update() self.search_button.disabled = disabled self.search_button.update() self.search_field.disabled = disabled self.search_field.update() self.search_box.disabled = disabled self.done_search() except (AttributeError, AssertionError): pass @property def options(self): return self.dropdown.options @options.setter def options(self, options): self._options = options self.dropdown.options = self._options @property def on_change(self): return self.dropdown.on_change @on_change.setter def on_change(self, on_change): self.dropdown.on_change = on_change def update_option_list(self, input: str): new_list_options = [] for option in self._options: if input.lower() in option.key.lower(): new_list_options.append(option) return new_list_options def on_search(self, e): if self.search_field.value.replace(' ', ''): self.dropdown.options = self.update_option_list(self.search_field.value) if len(self.dropdown.options) == 1: self.dropdown.value = self.dropdown.options[0].key self.on_change(e, label=self.label, value=self.value) else: self.dropdown.value = None else: self.dropdown.options = self._options self.dropdown.update() self.on_change() def search_now(self, e): self.search_box.width = self.search_width self.search_box.update() self.search_button.icon = 'highlight_remove' self.search_button.on_click = self.done_search self.search_button.update() self.search_field.border = "outline" self.search_field.update() self.search_field.focus() if self.search_field.value: self.on_search(e) def done_search(self, e=None): self.search_box.width = 0 self.search_box.update() self.search_button.icon = 'search' self.search_button.on_click = self.search_now self.search_button.update() self.search_field.border = "none" self.search_field.update() self.options = self._options self.dropdown.update() class MenuButton(ft.Container): def __init__( self, title: str, icon: Optional[ft.Control] = None, selected: bool = False, radio: Optional[ft.Radio] = None, ): super().__init__() self.icon = icon self.title = title self._selected = selected self.padding = ft.padding.only(left=43) self.height = 38 self.border_radius = 4 self.ink = True self.on_click = self.item_click self.radio = radio def item_click(self, _): pass def build(self): row = ft.Row() if self.icon is not None: row.controls.append(self.icon) if self.radio: row.controls.append(self.radio) else: row.controls.append(ft.Text(self.title)) self.content = row def _before_build_command(self): self.bgcolor = "surfacevariant" if self._selected else None super()._before_build_command() ================================================ FILE: kintree/gui/views/main.py ================================================ import os import copy import flet as ft # Version from ... import __version__ # Common view from .common import GUI_PARAMS, data_from_views from .common import DialogType from .common import CommonView from .common import DropdownWithSearch, SwitchWithRefs from .common import handle_transition # Tools from ...common.tools import cprint, download_with_retry # Settings from ...common import progress from ...config import settings, config_interface # InvenTree from ...database import inventree_interface # KiCad from ...kicad import kicad_interface # SnapEDA from ...search import snapeda_api # Main AppBar main_appbar = ft.AppBar( leading=ft.WindowDragArea( ft.Container( content=ft.Image( src=os.path.join(settings.PROJECT_DIR, 'gui', 'logo.ico'), fit=ft.ImageFit.CONTAIN, ), padding=ft.padding.only(left=10), expand=True, ), maximizable=True, ), leading_width=40, title=ft.WindowDragArea(ft.Container(ft.Text(f'Ki-nTree | {__version__}'), width=10000), maximizable=True), center_title=False, bgcolor=ft.colors.SURFACE_VARIANT, actions=[], ) # Navigation Controls MAIN_NAVIGATION = { 'Part Search': { 'nav_index': 0, 'route': '/main/part' }, 'InvenTree': { 'nav_index': 1, 'route': '/main/inventree' }, 'KiCad': { 'nav_index': 2, 'route': '/main/kicad' }, 'Create': { 'nav_index': 3, 'route': '/main/create' }, } # Load navigation indexes NAV_BAR_INDEX = {} for view in MAIN_NAVIGATION.values(): NAV_BAR_INDEX[view['nav_index']] = view['route'] # Main NavRail main_navrail = ft.NavigationRail( selected_index=0, label_type=ft.NavigationRailLabelType.ALL, min_width=100, min_extended_width=400, group_alignment=-0.9, destinations=[ ft.NavigationRailDestination( icon_content=ft.Icon(name=ft.icons.SCREEN_SEARCH_DESKTOP_OUTLINED, size=40), selected_icon_content=ft.Icon(name=ft.icons.SCREEN_SEARCH_DESKTOP_SHARP, size=40), label_content=ft.Text("Part Search", size=16), padding=10, ), ft.NavigationRailDestination( icon_content=ft.Icon(name=ft.icons.INVENTORY_2_OUTLINED, size=40), selected_icon_content=ft.Icon(name=ft.icons.INVENTORY_2, size=40), label_content=ft.Text("InvenTree", size=16), padding=10, ), ft.NavigationRailDestination( icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT_OUTLINED, size=40), selected_icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT, size=40), label_content=ft.Text("KiCad", size=16), padding=10, ), ft.NavigationRailDestination( icon_content=ft.Icon(name=ft.icons.BUILD_OUTLINED, size=40), selected_icon_content=ft.Icon(name=ft.icons.BUILD, size=40), label_content=ft.Text("Create", size=16), padding=10, ), ], on_change=None, ) class MainView(CommonView): '''Main view''' route = None data = None def __init__(self, page: ft.Page): # Get route self.route = MAIN_NAVIGATION[self.title].get('route', '/') # Init view super().__init__(page=page, appbar=main_appbar, navigation_rail=main_navrail) # Update application bar if not self.appbar.actions: self.appbar.actions.extend( [ ft.IconButton( ft.icons.SETTINGS, on_click=self.call_settings, ), ft.IconButton( ft.icons.CLOSE, on_click=lambda _: page.window.close(), ), ] ) else: self.appbar.actions[0].on_click = self.call_settings # Update navigation rail self.navigation_rail.on_change = self.nav_rail_redirect # Init data self.data = {} # Process enable switch if 'enable' in self.fields: self.fields['enable'].on_change = self.process_enable # Add floating button to reset view self.floating_action_button = ft.FloatingActionButton( icon=ft.icons.REPLAY, on_click=self.reset_view, ) def nav_rail_redirect(self, e): self._page.go(NAV_BAR_INDEX[e.control.selected_index]) def call_settings(self, e): handle_transition(self._page, transition=True) self._page.go('/settings') def reset_view(self, e, ignore=['enable'], hidden={}): def reset_field(field): if isinstance(field, ft.ProgressBar): field.value = 0 else: field.value = None for name, field in self.fields.items(): if isinstance(field, dict): for key, value in field.items(): value.disabled = True reset_field(value) else: if name not in ignore: reset_field(field) if hidden: for key, value in hidden.items(): if not value: self.data[key] = value else: self.data[key] = None # Clear data self.push_data() self._page.update() def partial_update(self): '''Process partial view updates''' return def process_enable(self, e, value=None, ignore=['enable']): disabled = False if e.data.lower() == 'false': disabled = True # Overwrite with value if value is not None: disabled = not value key = e.control.label.lower() settings.set_enable_flag(key, not disabled) for name, field in self.fields.items(): if name not in ignore: field.disabled = disabled field.update() self.push_data(e) def sanitize_data(self): return def push_data(self, e=None, hidden={}): for key, field in self.fields.items(): try: self.data[key] = field.value except AttributeError: pass if hidden: for key, value in hidden.items(): self.data[key] = value # Sanitize data before pushing self.sanitize_data() # Push data_from_views[self.title] = self.data def did_mount(self, enable=False): handle_transition(self._page, transition=False, update_page=True) if self.fields.get('enable', None) is not None: # Create enable event e = ft.ControlEvent( target=None, name='did_mount_enable', data='true' if enable else 'false', page=self._page, control=self.fields['enable'], ) # Process enable self.process_enable(e) return super().did_mount() class PartSearchView(MainView): '''Part search view''' title = 'Part Search' # List of search fields search_fields_list = [ 'name', 'description', 'revision', 'keywords', 'supplier_name', 'supplier_part_number', 'supplier_link', 'manufacturer_name', 'manufacturer_part_number', 'datasheet', 'image', ] fields = { 'part_number': ft.TextField( label="Part Number", dense=True, hint_text="Part Number", width=250, expand=True, ), 'supplier': ft.Dropdown( label="Supplier", dense=True, width=250 ), 'search_button': ft.IconButton( icon=ft.icons.SEND, icon_color="blue900", icon_size=32, height=48, width=48, tooltip="Submit", ), 'parameter_view': ft.Switch( label='View Parameters', disabled=True ), 'search_form': {}, 'parameter_form': {}, } def reset_view(self, e, ignore=['enable']): hidden_fields = { 'searched_part_number': '', 'custom_part': None, } self.fields['parameter_form'] = {} try: self.fields['part_number'].focus() except AssertionError: pass return super().reset_view(e, ignore=ignore, hidden=hidden_fields) def enable_search_fields(self): for form_field in self.fields['search_form'].values(): form_field.disabled = False self.fields['parameter_view'].disabled = False self._page.update() return def run_search(self, e): # Reset view self.reset_view(e, ignore=['part_number', 'supplier']) self.switch_view() # Validate form 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: if not self.fields['part_number'].value: error_msg = 'Missing Part Number' else: error_msg = 'Missing Supplier' self.show_dialog( d_type=DialogType.ERROR, message=error_msg, ) else: self.fields['part_number'].value = self.fields['part_number'].value.strip() self._page.splash.visible = True self._page.update() if not self.fields['part_number'].value and not self.fields['supplier'].value: self.data['custom_part'] = True self.enable_search_fields() else: self.data['custom_part'] = False # Get supplier supplier = inventree_interface.get_supplier_name(self.fields['supplier'].value) # Supplier search part_supplier_info = inventree_interface.supplier_search( supplier, self.fields['part_number'].value ) part_supplier_form = None if part_supplier_info: # Translate to user form format part_supplier_form = inventree_interface.translate_supplier_to_form( supplier=supplier, part_info=part_supplier_info, ) if part_supplier_form: for field_idx, field_name in enumerate(self.fields['search_form'].keys()): # print(field_idx, field_name, get_default_search_keys()[field_idx], search_form_field[field_name]) try: self.fields['search_form'][field_name].value = part_supplier_form.get(field_name, '') except IndexError: pass # Enable editing self.enable_search_fields() # Stitch parameters if part_supplier_info.get('parameters', None): self.data['parameters'] = part_supplier_info['parameters'] for parameter, value in self.data['parameters'].items(): text_field = ft.TextField( label=parameter, value=value, expand=True, on_change=self.push_data, ) self.fields['parameter_form'][parameter] = text_field # and pricing if part_supplier_info.get('pricing', None): self.data['pricing'] = part_supplier_info['pricing'] self.data['currency'] = part_supplier_info.get('currency', None) # Add to data buffer self.push_data() self._page.splash.visible = False if not self.data['supplier_part_number'] and not self.data['custom_part']: self.show_dialog( d_type=DialogType.ERROR, message='Part not found', ) elif not self.data['manufacturer_part_number']: self.show_dialog( d_type=DialogType.ERROR, message='Found part has no manufacturer part number', ) elif self.data['searched_part_number'].lower() != self.data['manufacturer_part_number'].lower(): self.show_dialog( d_type=DialogType.WARNING, message='Found manufacturer part number does not match the requested part number', ) self._page.update() return def push_data(self, e=None): hidden_fields = { 'searched_part_number': self.fields['part_number'].value, 'custom_part': self.data.get('custom_part', None), } for key, field in self.fields['search_form'].items(): self.data[key] = field.value for key, field in self.fields['parameter_form'].items(): self.data['parameters'][key] = field.value return super().push_data(e, hidden=hidden_fields) def partial_update(self): # Update supplier options self.update_suppliers() def update_suppliers(self): # Reload suppliers self.fields['supplier'].options = [ ft.dropdown.Option(supplier) for supplier in settings.SUPPORTED_SUPPLIERS_API ] if len(self.fields['supplier'].options) == 1: self.fields['supplier'].value = self.fields['supplier'].options[0].key else: self.fields['supplier'].value = None try: self.fields['supplier'].update() except AssertionError: # Control not added to page yet pass def switch_view(self, e=None): # show parameters instead of part information parameters_view = self.fields['parameter_view'].value self.column.controls[0].content.controls = [ ft.Row(), ft.Row( controls=[ self.fields['part_number'], self.fields['supplier'], self.fields['search_button'], self.fields['parameter_view'], ], ), ft.Divider(), ] if not parameters_view: for field, text_field in self.fields['search_form'].items(): self.column.controls[0].content.controls.append(ft.Row([text_field])) else: for field, text_field in self.fields['parameter_form'].items(): self.column.controls[0].content.controls.append(ft.Row([text_field])) self._page.update() def perform_pn_search(self, e): self.run_search(e) try: self.fields['part_number'].focus() except AssertionError: pass def build_column(self): self.update_suppliers() # Enable search method self.fields['search_button'].on_click = self.run_search self.fields['parameter_view'].on_change = self.switch_view self.fields['part_number'].on_submit = self.perform_pn_search self.column = ft.Column( controls=[ ft.Container( content=ft.Column( controls=[ ft.Row(), ft.Row( controls=[ self.fields['part_number'], self.fields['supplier'], self.fields['search_button'], self.fields['parameter_view'], ], ), ft.Divider(), ], scroll=ft.ScrollMode.HIDDEN, ), expand=True, ), ], alignment=ft.MainAxisAlignment.END, expand=True, ) # Create search form for field in self.search_fields_list: label = field.replace('_', ' ').title() text_field = ft.TextField( label=label, dense=True, hint_text=label, disabled=True, expand=True, on_change=self.push_data, ) self.column.controls[0].content.controls.append(ft.Row([text_field])) self.fields['search_form'][field] = text_field def did_mount(self, enable=False): if ( not self.fields['part_number'].value and self.fields['supplier'].value is None and self.data.get('custom_part', None) is None ): self.show_dialog( d_type=DialogType.WARNING, message='To create a Custom Part click on the Submit button', ) return super().did_mount(enable) class InventreeView(MainView): '''InvenTree categories view''' title = 'InvenTree' fields = { 'enable': ft.Switch( label='InvenTree', value=settings.ENABLE_INVENTREE, ), 'alternate': ft.Switch( label='Update existing', value=settings.ENABLE_ALTERNATE if settings.ENABLE_INVENTREE else False, disabled=not settings.ENABLE_INVENTREE, ), 'load_categories': ft.ElevatedButton( 'Reload InvenTree Categories', width=GUI_PARAMS['button_width'] * 2.6, height=36, icon=ft.icons.REPLAY, disabled=False, ), 'load_stock_locations': ft.ElevatedButton( 'Reload InvenTree Stock locations', width=GUI_PARAMS['button_width'] * 2.8, height=36, icon=ft.icons.REPLAY, disabled=False, ), 'Category': DropdownWithSearch( label='Category', dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], disabled=settings.ENABLE_ALTERNATE, options=[], ), 'IPN: Category Code': ft.Dropdown( label='IPN: Category Code', width=GUI_PARAMS['textfield_width'] / 2 - 5, dense=GUI_PARAMS['textfield_dense'], # disabled=settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False), options=[], ), 'Create New Code': SwitchWithRefs( label='Create New Code', ), 'check_existing': ft.Switch( label='Check for existing Parts', value=settings.CHECK_EXISTING if settings.ENABLE_INVENTREE else False, disabled=not settings.ENABLE_INVENTREE, ), 'New Category Code': ft.TextField( label='New Category Code', width=GUI_PARAMS['textfield_width'] / 2 - 5, dense=GUI_PARAMS['textfield_dense'], visible=False, ), 'Existing Part ID': ft.TextField( label='Existing Part ID', width=GUI_PARAMS['textfield_width'] / 2 - 5, dense=GUI_PARAMS['textfield_dense'], visible=True, ), 'Existing Part IPN': ft.TextField( label='Existing Part IPN', width=GUI_PARAMS['textfield_width'] / 2 - 5, dense=GUI_PARAMS['textfield_dense'], visible=True, ), 'Update Parameter': SwitchWithRefs( label='Update Parameter', value=settings.UPDATE_INVENTREE if settings.ENABLE_INVENTREE else False, disabled=not settings.ENABLE_INVENTREE, ), 'Create stock': SwitchWithRefs( label='Create Stock', disabled=not settings.ENABLE_INVENTREE, ), 'Stock location': DropdownWithSearch( label='Stock Location', disabled=not settings.ENABLE_INVENTREE, dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], options=[], ), 'Stock quantity': ft.TextField( label='Stock Quantity', disabled=not settings.ENABLE_INVENTREE, keyboard_type=ft.KeyboardType.NUMBER, value='1', ), 'Make stock location default': ft.Checkbox( label="Set this location as the part\'s default location", disabled=not settings.ENABLE_INVENTREE, value=False, ), } def __init__(self, page: ft.Page): self.category_row_ref = ft.Ref[ft.Row]() self.ipncode_row_ref = ft.Ref[ft.Row]() self.alternate_row_ref = ft.Ref[ft.Row]() self.create_stock_widgets_ref = ft.Ref[ft.Row]() super().__init__(page) def partial_update(self): # Update IPN row self.process_ipncode() def sanitize_data(self): category_tree = self.data.get('Category', None) if category_tree: self.data['Category'] = inventree_interface.split_category_tree(category_tree) stock_location_tree = self.data.get('Stock location', None) if stock_location_tree: self.data['Stock location'] = inventree_interface.split_category_tree(stock_location_tree) def process_enable(self, e): inventree_enable = True # Switch control: override if e.data.lower() == 'false': inventree_enable = False super().process_enable(e, value=inventree_enable, ignore=['enable', 'IPN: Category Code']) if not inventree_enable: # If InvenTree disabled self.fields['alternate'].value = inventree_enable self.fields['alternate'].update() self.process_alternate(e, value=inventree_enable) self.process_create_stock(e, value=inventree_enable) else: alternate_enable = self.fields['alternate'].value self.process_alternate(e, value=alternate_enable) stock_create_enabled = self.fields['Create stock'].value self.process_create_stock(e, value=stock_create_enabled) self.process_ipncode() def process_alternate(self, e, value=None): if value is not None: alt_visible = value else: # Switch control # Reset view self.reset_view(e, ignore=['enable', 'alternate']) self.fields['New Category Code'].visible = False # Get switch value alt_visible = False if e.data.lower() == 'true': alt_visible = True # Load category button self.fields['load_categories'].disabled = alt_visible self.fields['load_categories'].update() # Category row visibility self.category_row_ref.current.visible = not alt_visible self.category_row_ref.current.update() # Alternate row visibility self.alternate_row_ref.current.visible = alt_visible self.alternate_row_ref.current.update() # Update settings settings.set_enable_flag('alternate', alt_visible) settings.set_enable_flag('update', alt_visible) # User dialog if alt_visible: self.show_dialog( d_type=DialogType.WARNING, message='Alternate Mode Enabled: Enter Existing Part ID or Part IPN', ) self.push_data(e) def process_update(self, e, value=None): if value is not None: update_enabled = value else: # Get switch value update_enabled = False if e.data.lower() == 'true': update_enabled = True settings.set_enable_flag('update', update_enabled) self.push_data(e) def process_button(self, e, value=None): if value is not None: button_enabled = value else: # Get switch value button_enabled = False if e.data.lower() == 'true': button_enabled = True if e.control.label == 'Update existing': settings.set_enable_flag('update', button_enabled) elif e.control.label == 'Check for existing Parts': settings.set_enable_flag('check_existing', button_enabled) self.push_data(e) def process_category(self, e=None, label=None, value=None): parent_category = None if isinstance(self.fields['Category'].value, str): parent_category = inventree_interface.split_category_tree(self.fields['Category'].value)[0] # Check for category codes options = self.get_code_options() if options: self.fields['IPN: Category Code'].options = options # Select category code corresponding to selected category code = config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES'].get(parent_category, None) if code and not self.fields['Create New Code'].value: self.fields['IPN: Category Code'].value = code self.fields['IPN: Category Code'].update() self.push_data(e) def process_location(self, e=None, label=None, value=None): self.fields['Stock location'].options = self.get_stock_location_options() self.push_data(e) def process_ipncode(self): ipncode_enable = bool( settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', False) and settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False) ) self.ipncode_row_ref.current.visible = ipncode_enable self.ipncode_row_ref.current.update() def process_create_stock(self, e, value=None): if value is not None: create_stock_visible = value else: self.fields['New Category Code'].visible = False # Get switch value create_stock_visible = False if e.data.lower() == 'true': create_stock_visible = True # Stock create row visibility self.create_stock_widgets_ref.current.visible = create_stock_visible self.create_stock_widgets_ref.current.update() def get_code_options(self): try: return [ ft.dropdown.Option(code) for code in config_interface.load_file(settings.CONFIG_CATEGORIES)['CODES'].values() ] except AttributeError: return [] def get_category_options(self, reload=False): return [ ft.dropdown.Option(category) for category in inventree_interface.build_category_tree(reload=reload) ] def get_stock_location_options(self, reload=False): return [ ft.dropdown.Option(location) for location in inventree_interface.build_stock_location_tree(reload=reload) ] def reload_categories(self, e): self._page.splash.visible = True self._page.update() # Check connection if not inventree_interface.connect_to_server(): self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server') else: self.fields['Category'].options = self.get_category_options(reload=True) self.fields['Category'].update() self._page.splash.visible = False self._page.update() def reload_stock_locations(self, e): self._page.splash.visible = True self._page.update() # Check connection if not inventree_interface.connect_to_server(): self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server') else: self.fields['Stock location'].options = self.get_stock_location_options(reload=True) self.fields['Stock location'].update() self._page.splash.visible = False self._page.update() def create_ipn_code(self, e): # Get switch value new_code = True if e.data.lower() == 'false': new_code = False self.fields['IPN: Category Code'].disabled = new_code self.fields['IPN: Category Code'].update() if not new_code: self.process_category() else: self.push_data(e) def build_column(self): # Update dropdown with category options self.fields['Category'].options = self.get_category_options() self.fields['Category'].on_change = self.process_category self.fields['load_categories'].on_click = self.reload_categories # Category codes self.fields['IPN: Category Code'].options = self.get_code_options() self.fields['IPN: Category Code'].on_change = self.push_data self.fields['Create New Code'].on_change = self.create_ipn_code self.fields['New Category Code'].on_change = self.push_data # Other Settings self.fields['check_existing'].on_change = self.process_button # Alternate fields self.fields['alternate'].on_change = self.process_alternate self.fields['Existing Part ID'].on_change = self.push_data self.fields['Existing Part IPN'].on_change = self.push_data self.fields['Update Parameter'].on_change = self.process_update # Create stock location self.fields['Stock location'].options = self.get_stock_location_options() self.fields['Stock location'].on_change = self.process_location self.fields["Create stock"].on_change = self.process_create_stock self.fields['Stock location'].on_change = self.push_data self.fields['Stock quantity'].on_change = self.push_data self.fields['Make stock location default'].on_change = self.push_data self.fields['load_stock_locations'].on_click = self.reload_stock_locations self.column = ft.Column( controls=[ ft.Row(), ft.Row( [ self.fields['enable'], self.fields['alternate'], self.fields['load_categories'], ], width=GUI_PARAMS['dropdown_width'], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, ), ft.Row( ref=self.category_row_ref, controls=[ ft.Column( [ ft.Row([self.fields['Category'],]), ft.Row( ref=self.ipncode_row_ref, controls=[ ft.Column( [ ft.Row( [ self.fields['IPN: Category Code'], self.fields['Create New Code'], ] ), ft.Row([self.fields['New Category Code']]), ], ), ], ), ft.Row( [ self.fields['check_existing'], ], ), ], ), ], ), ft.Column( ref=self.alternate_row_ref, controls=[ ft.Row( controls=[ self.fields['Existing Part ID'], self.fields['Existing Part IPN'], ], ), ft.Row( controls=[self.fields['Update Parameter']] ) ] ), ft.Column( ref=self.create_stock_widgets_ref, controls=[ ft.Row( controls=[ self.fields['Create stock'], self.fields['load_stock_locations'] ] ) ] ), ft.Column( ref=self.create_stock_widgets_ref, controls=[ ft.Row( controls=[self.fields['Stock location']], ), ft.Row( controls=[self.fields['Stock quantity']], ), ft.Row( controls=[self.fields['Make stock location default']], ), ] ) ], ) # Connect New Category Code fields cc_ref = ft.Ref[ft.TextField]() cc_ref.current = self.fields['New Category Code'] self.fields['Create New Code'].refs = [cc_ref] def did_mount(self): return super().did_mount(enable=settings.ENABLE_INVENTREE) class KicadView(MainView): '''KiCad view''' title = 'KiCad' fields = { 'enable': ft.Switch( label='KiCad', value=settings.ENABLE_KICAD, ), 'Symbol Library': DropdownWithSearch( label='', dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], options=[], ), 'Symbol Template': DropdownWithSearch( label='', dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], options=[], ), 'Footprint Library': DropdownWithSearch( label='', dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], options=[], ), 'Footprint': DropdownWithSearch( label='', dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], dense=GUI_PARAMS['textfield_dense'], options=[], ), 'New Footprint': SwitchWithRefs( label='New Footprint', ), 'New Footprint Name': ft.TextField( label='New Footprint Name', width=GUI_PARAMS['textfield_width'], dense=GUI_PARAMS['textfield_dense'], visible=False, ), 'Check SnapEDA': ft.ElevatedButton( content=ft.Row( [ ft.Icon('search'), ft.Text('Check SnapEDA', size=16), ] ), height=GUI_PARAMS['button_height'], width=GUI_PARAMS['button_width'] * 2, ), } def build_alert_dialog(self, symbol: str, footprint: str, download: str, single_result=False): modal_content = ft.Row() modal_msg = ft.Text('Symbol and footprint are not available on SnapEDA') # Build content if symbol: modal_content.controls.append(ft.Image(symbol)) modal_msg = ft.Text('Symbol is available on SnapEDA') if footprint: modal_content.controls.append(ft.Image(footprint)) if symbol: modal_msg = ft.Text('Symbol and footprint are available on SnapEDA') else: modal_msg = ft.Text('Footprint is available on SnapEDA') # Build actions modal_actions = [] if download: if not symbol and not footprint: if single_result: modal_actions.append(ft.TextButton('Check Part', on_click=lambda _: self._page.launch_url(download))) else: modal_msg = ft.Text('Multiple matches found on SnapEDA') modal_actions.append(ft.TextButton('See Results', on_click=lambda _: self._page.launch_url(download))) else: modal_actions.append(ft.TextButton('Download', on_click=lambda _: self._page.launch_url(download))) modal_actions.append(ft.TextButton('Close', on_click=lambda _: self.show_dialog(open=False))) return ft.AlertDialog( modal=True, title=modal_msg, content=modal_content, actions=modal_actions, actions_alignment=ft.MainAxisAlignment.END, # on_dismiss=None, ) def process_enable(self, e, value=None, ignore=['enable']): super().process_enable(e, value, ignore) if self.fields['enable'].value: self.fields['Footprint'].disabled = self.fields['New Footprint'].value self.fields['Footprint'].update() def push_data(self, e=None, label=None, value=None): super().push_data(e) if label or e: try: if 'Footprint Library' in [label, e.control.label]: if value: selected_footprint_library = value else: selected_footprint_library = e.data self.update_footprint_options(selected_footprint_library) except AttributeError: # Handles condition where search field tries to reset dropdown pass def check_snapeda(self, e): if not data_from_views.get('Part Search', {}).get('manufacturer_part_number', ''): self.show_dialog( d_type=DialogType.ERROR, message='Missing Manufacturer Part Number', ) return self._page.splash.visible = True self._page.update() response = snapeda_api.fetch_snapeda_part_info(data_from_views['Part Search']['manufacturer_part_number']) data = snapeda_api.parse_snapeda_response(response) images = {} if data['has_symbol'] or data['has_footprint']: images = snapeda_api.download_snapeda_images(data) self._page.splash.visible = False self._page.update() self.dialog = self.build_alert_dialog( images.get('symbol', ''), images.get('footprint', ''), data.get('part_url', ''), data.get('has_single_result', False), ) self.show_dialog(snackbar=False, open=True) def update_footprint_options(self, library: str): footprint_options = [] if library is None: return footprint_options # Load paths footprint_paths = self.get_footprint_libraries() # Get path matching selected footprint library footprint_lib_path = footprint_paths[library] # Load footprints footprints = [ item.replace('.kicad_mod', '') for item in sorted(os.listdir(footprint_lib_path)) if os.path.isfile(os.path.join(footprint_lib_path, item)) ] # Find folder matching value for footprint in footprints: footprint_options.append(ft.dropdown.Option(footprint)) self.fields['Footprint'].options = footprint_options self.fields['Footprint'].update() def get_footprint_libraries(self) -> dict: footprint_libraries = {} try: for folder in sorted(os.listdir(settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'])): if os.path.isdir(os.path.join(settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'], folder)): footprint_libraries[folder.replace('.pretty', '')] = os.path.join( settings.KICAD_SETTINGS['KICAD_FOOTPRINTS_PATH'], folder ) except FileNotFoundError: pass return footprint_libraries def find_libraries(self, type: str) -> list: found_libraries = [] if type == 'symbol': try: found_libraries = [ file.replace('.kicad_sym', '') for file in sorted(os.listdir(settings.KICAD_SETTINGS['KICAD_SYMBOLS_PATH'])) if file.endswith('.kicad_sym') ] except FileNotFoundError: pass elif type == 'template': templates = config_interface.load_templates_paths( user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP, template_path=settings.KICAD_SETTINGS['KICAD_TEMPLATES_PATH'] ) for key in templates: for template in templates[key]: found_libraries.append(f'{key}/{template}') elif type == 'footprint': found_libraries = list(self.get_footprint_libraries().keys()) return found_libraries def build_library_options(self, type: str): options = [] found_libraries = self.find_libraries(type) if found_libraries: options = [ft.dropdown.Option(lib_name) for lib_name in found_libraries] return options def create_footprint(self, e): # Get switch value new_footprint = True if e.data.lower() == 'false': new_footprint = False self.fields['Footprint'].disabled = new_footprint self.fields['Footprint'].update() if not new_footprint: self.update_footprint_options(self.fields['Footprint Library'].value) self.push_data(e) def build_column(self): # Library options checks self.checks = [] self.column = ft.Column( controls=[ft.Row()], alignment=ft.MainAxisAlignment.START, expand=True, ) kicad_inputs = [] for name, field in self.fields.items(): # Update callbacks if isinstance(field, ft.ElevatedButton): field.on_click = self.check_snapeda # Update options elif isinstance(field, DropdownWithSearch): field.label = name if name == 'Symbol Library': field.options = self.build_library_options(type='symbol') elif name == 'Symbol Template': field.options = self.build_library_options(type='template') elif name == 'Footprint Library': field.options = self.build_library_options(type='footprint') if not field.options and name != 'Footprint': self.checks.append(f'KiCad {name} path does not exists or folder is empty') if name != 'enable': field.on_change = self.push_data kicad_inputs.append(field) self.column.controls.extend(kicad_inputs) # Connect New Footprint fields fp_ref = ft.Ref[ft.TextField]() fp_ref.current = self.fields['New Footprint Name'] self.fields['New Footprint'].refs = [fp_ref] self.fields['New Footprint'].on_change = self.create_footprint def did_mount(self): if 'InvenTree' in data_from_views: # Get value of alternate switch if data_from_views['InvenTree'].get('alternate', False): self.fields['enable'].disabled = True self.fields['enable'].value = False self.show_dialog( d_type=DialogType.ERROR, message='InvenTree Alternate switch is enabled', ) return super().did_mount(enable=False) else: self.fields['enable'].disabled = False # Process checks if self.checks: error_msg = f'{self.checks[0]}' for check in self.checks[1:]: error_msg += f'\n{check}' self.show_dialog( d_type=DialogType.ERROR, message=error_msg, ) return super().did_mount(enable=settings.ENABLE_KICAD) class CreateView(MainView): '''Create view''' title = 'Create' fields = { 'inventree_progress': ft.ProgressBar(height=32, width=420, value=0), 'kicad_progress': ft.ProgressBar(height=32, width=420, value=0), 'create': ft.ElevatedButton( content=ft.Row( [ ft.Icon('build_circle'), ft.Text('Create Part', size=20), ft.Icon('build_circle'), ] ), height=GUI_PARAMS['button_height'], width=GUI_PARAMS['button_width'] * 2, ), 'cancel': ft.ElevatedButton( content=ft.Row( [ ft.Icon('highlight_remove'), ft.Text('Cancel', size=20), ft.Icon('highlight_remove'), ] ), height=GUI_PARAMS['button_height'], width=GUI_PARAMS['button_width'] * 1.6, bgcolor=ft.colors.RED_50, disabled=True, ), } inventree_progress_row = None kicad_progress_row = None create_continue = True def show_dialog(self, type: DialogType, message: str): if 'create' in self.fields: self.enable_create(True) return super().show_dialog(type, message) def enable_create(self, enable=True): self.fields['create'].disabled = not enable self.fields['create'].update() # Invert cancel button self.enable_cancel(enable=not enable) def enable_cancel(self, enable=True): if enable: for item in self.fields['cancel'].content.controls: item.color = ft.colors.RED_ACCENT_700 else: for item in self.fields['cancel'].content.controls: item.color = None self.fields['cancel'].disabled = not enable self.fields['cancel'].update() def cancel(self, e=None): self.create_continue = False def process_cancel(self): # if settings.ENABLE_INVENTREE: # if self.fields['inventree_progress'].value < 1.0: # self.fields['inventree_progress'].color = "red" # self.fields['inventree_progress'].update() # if settings.ENABLE_KICAD: # if self.fields['kicad_progress'].value < 1.0: # self.fields['kicad_progress'].color = "red" # self.fields['kicad_progress'].update() self.show_dialog(DialogType.ERROR, 'Action Cancelled') self.create_continue = True self.enable_create(True) return def reset_progress_bars(self): # Setup progress bars if not settings.ENABLE_INVENTREE: self.inventree_progress_row.current.visible = False else: self.inventree_progress_row.current.visible = True # Reset progress bar progress.reset_progress_bar(self.fields['inventree_progress']) self.inventree_progress_row.current.update() if not settings.ENABLE_KICAD: self.kicad_progress_row.current.visible = False else: self.kicad_progress_row.current.visible = True # Reset progress bar progress.reset_progress_bar(self.fields['kicad_progress']) self.kicad_progress_row.current.update() if not settings.ENABLE_INVENTREE and not settings.ENABLE_KICAD: self.fields['create'].disabled = True else: self.fields['create'].disabled = False self.fields['create'].update() def create_part(self, e=None): self.reset_progress_bars() if not settings.ENABLE_INVENTREE and not settings.ENABLE_KICAD: self.show_dialog(DialogType.ERROR, 'Both InvenTree and KiCad are disabled (nothing to create)') # print('data_from_views='); cprint(data_from_views) # Check data is present if not data_from_views.get('Part Search', None): self.show_dialog(DialogType.ERROR, 'Missing Part Data (nothing to create)') return # Custom part check part_info = copy.deepcopy(data_from_views['Part Search']) custom = part_info.pop('custom_part') # Part number check part_number = data_from_views['Part Search'].get('manufacturer_part_number', None) if not custom: if not part_number: self.show_dialog(DialogType.ERROR, 'Missing Manufacturer Part Number') return else: # Update IPN (later overwritten) part_info['IPN'] = part_number # Button update self.enable_create(False) # KiCad data gathering symbol = None template = None footprint = None if settings.ENABLE_KICAD and not settings.ENABLE_ALTERNATE: # Check data is present if not data_from_views.get('KiCad', None): self.show_dialog(DialogType.ERROR, 'Missing KiCad Data') return # Process symbol symbol_lib = data_from_views['KiCad'].get('Symbol Library', None) if symbol_lib: symbol = f"{symbol_lib}:{part_number}" # Process template template = data_from_views['KiCad'].get('Symbol Template', None) # Process footprint footprint_lib = data_from_views['KiCad'].get('Footprint Library', None) if footprint_lib: if data_from_views['KiCad'].get('New Footprint', False): new_footprint = data_from_views['KiCad'].get('New Footprint Name', 'TBD') footprint = f"{footprint_lib}:{new_footprint}" elif data_from_views['KiCad'].get('Footprint', None): footprint = f"{footprint_lib}:{data_from_views['KiCad']['Footprint']}" else: pass # print(symbol, template, footprint) if not symbol or not template or not footprint: self.show_dialog(DialogType.ERROR, 'Missing KiCad Data') return if not self.create_continue: return self.process_cancel() # InvenTree data processing if settings.ENABLE_INVENTREE: # Check data is present if not data_from_views.get('InvenTree', None): self.show_dialog(DialogType.ERROR, 'Missing InvenTree Data') return # Check connection if not inventree_interface.connect_to_server(): self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server') return if settings.ENABLE_ALTERNATE: # Check mandatory data if not data_from_views['InvenTree']['Existing Part ID'] and not data_from_views['InvenTree']['Existing Part IPN']: self.show_dialog(DialogType.ERROR, 'Missing Existing Part ID and Part IPN') return # Create alternate alt_result = inventree_interface.inventree_create_alternate( part_info=part_info, part_id=data_from_views['InvenTree']['Existing Part ID'], part_ipn=data_from_views['InvenTree']['Existing Part IPN'], show_progress=self.fields['inventree_progress'], ) else: # Check mandatory data if not data_from_views['Part Search'].get('name', None): self.show_dialog(DialogType.ERROR, 'Missing Part Name') return if len(data_from_views['Part Search'].get('name', None)) > 100: self.show_dialog(DialogType.ERROR, 'Part Name too long (>100 characters)') return if not data_from_views['Part Search'].get('description', None): self.show_dialog(DialogType.ERROR, 'Missing Part Description') return # Get relevant data category_tree = data_from_views['InvenTree'].get('Category', None) if not category_tree: # Check category is present self.show_dialog(DialogType.ERROR, 'Missing InvenTree Category') return else: part_info['category_tree'] = category_tree # Category code if settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False): if data_from_views['InvenTree'].get('Create New Code', False): part_info['category_code'] = data_from_views['InvenTree'].get('New Category Code', '') else: part_info['category_code'] = data_from_views['InvenTree'].get('IPN: Category Code', '') stock = None if data_from_views['InvenTree'].get('Create stock'): stock_tree = data_from_views['InvenTree'].get('Stock location', None) if not stock_tree: # Check category is present self.show_dialog(DialogType.ERROR, 'Missing InvenTree Stock location') return stock = { 'location': inventree_interface.get_inventree_stock_location_id(data_from_views['InvenTree'].get('Stock location')), 'quantity': data_from_views['InvenTree'].get('Stock quantity'), 'make_default': data_from_views['InvenTree'].get('Make stock location default'), } # Create new part new_part, part_pk, part_info = inventree_interface.inventree_create( part_info=part_info, kicad=settings.ENABLE_KICAD, symbol=symbol, footprint=footprint, show_progress=self.fields['inventree_progress'], is_custom=custom, stock=stock, ) # print(new_part, part_pk) # cprint(part_info) if settings.ENABLE_ALTERNATE: if alt_result: # Update InvenTree URL if data_from_views['InvenTree']['Existing Part IPN']: part_ref = data_from_views['InvenTree']['Existing Part IPN'] else: part_ref = data_from_views['InvenTree']['Existing Part ID'] part_info['inventree_url'] = f'{settings.PART_URL_ROOT}{part_ref}/' else: self.fields['inventree_progress'].color = "amber" # Complete add operation self.fields['inventree_progress'].value = progress.MAX_PROGRESS else: if part_pk: # Update symbol if symbol: symbol = f'{symbol.split(":")[0]}:{part_info["IPN"]}' self.fields['inventree_progress'].color = 'green' if not new_part: self.fields['inventree_progress'].color = 'amber' # Complete add operation self.fields['inventree_progress'].value = progress.MAX_PROGRESS else: self.fields['inventree_progress'].color = 'red' self.fields['inventree_progress'].update() if not self.create_continue: return self.process_cancel() # KiCad data processing if settings.ENABLE_KICAD and not settings.ENABLE_ALTERNATE: # Store "pseudo-category" as re-used in multiple places pseudo_category = symbol.split(':')[0] # Translate part info if InvenTree not enabled if not settings.ENABLE_INVENTREE: part_info = inventree_interface.translate_form_to_inventree( part_info=part_info, category_tree=[pseudo_category], is_custom=custom, ) # Also add datasheet URL as part page URL part_info['inventree_url'] = part_info['datasheet'] part_info['Symbol'] = symbol part_info['Template'] = template.split('/') part_info['Footprint'] = footprint symbol_library_path = os.path.join( settings.KICAD_SETTINGS['KICAD_SYMBOLS_PATH'], f'{pseudo_category}.kicad_sym', ) # Reset progress progress.CREATE_PART_PROGRESS = 0 # Add part symbol to KiCAD cprint('\n[MAIN]\tAdding part to KiCad', silent=settings.SILENT) kicad_success, kicad_new_part, kicad_part_name = kicad_interface.inventree_to_kicad( part_data=part_info, library_path=symbol_library_path, show_progress=self.fields['kicad_progress'], ) # print(kicad_success, kicad_new_part) # Update symbol name in InvenTree if settings.ENABLE_INVENTREE and part_pk: old_state = settings.UPDATE_INVENTREE settings.UPDATE_INVENTREE = True inventree_interface.inventree_process_parameters( part_pk, {'Symbol': f"{symbol_lib}:{kicad_part_name}"}, show_progress=self.fields['inventree_progress'], ) settings.UPDATE_INVENTREE = old_state # Complete add operation if kicad_success: self.fields['kicad_progress'].color = 'green' if not kicad_new_part: self.fields['kicad_progress'].color = 'amber' self.fields['kicad_progress'].update() self.fields['kicad_progress'].value = progress.MAX_PROGRESS self.fields['kicad_progress'].update() else: self.fields['kicad_progress'].color = 'red' self.fields['kicad_progress'].update() if not self.create_continue: return self.process_cancel() # Final operations # Download a local version of the part datasheet if settings.DATASHEET_SAVE_ENABLED: filename = os.path.join( settings.DATASHEET_SAVE_PATH, f'{part_info.get("IPN", "datasheet")}.pdf', ) if settings.DATASHEET_UPLOAD and os.path.isfile(filename): # Datasheet was already downloaded cprint('\n[MAIN]\tDatasheet') cprint(f'[INFO]\tSuccess: Datasheet file exists ({filename})') else: # Datasheet needs to be downloaded datasheet_url = part_info.get('datasheet', None) if datasheet_url: cprint('\n[MAIN]\tDownloading Datasheet') if download_with_retry(datasheet_url, filename, filetype='PDF', timeout=10): cprint(f'[INFO]\tSuccess: Datasheet saved to {filename}') # Open browser if settings.ENABLE_INVENTREE: if part_info.get('inventree_url', None): if settings.AUTOMATIC_BROWSER_OPEN: # Auto-Open Browser Window cprint( f'\n[MAIN]\tOpening URL {part_info["inventree_url"]} in browser', silent=settings.SILENT ) try: self._page.launch_url(part_info['inventree_url']) except TypeError: cprint('[INFO]\tError: Failed to open URL', silent=settings.SILENT) else: cprint(f'\n[MAIN]\tPart page URL: {part_info["inventree_url"]}', silent=settings.SILENT) # Button update self.enable_create(True) def build_column(self): self.inventree_progress_row = ft.Ref[ft.Row]() self.kicad_progress_row = ft.Ref[ft.Row]() # Update callbacks self.fields['create'].on_click = self.create_part self.fields['cancel'].on_click = self.cancel self.column = ft.Column( controls=[ ft.Row(), ft.Row( controls=[ self.fields['create'], self.fields['cancel'], ], alignment=ft.MainAxisAlignment.CENTER, width=600, ), ft.Row(height=16), ft.Row( ref=self.inventree_progress_row, controls=[ ft.Icon(ft.icons.INVENTORY_2, size=32), ft.Text('InvenTree', size=20, weight=ft.FontWeight.BOLD, width=120), self.fields['inventree_progress'], ], width=600, visible=settings.ENABLE_INVENTREE, ), ft.Row( ref=self.kicad_progress_row, controls=[ ft.Icon(ft.icons.SETTINGS_INPUT_COMPONENT, size=32), ft.Text('KiCad', size=20, weight=ft.FontWeight.BOLD, width=120), self.fields['kicad_progress'], ], width=600, visible=settings.ENABLE_KICAD, ), ], ) def did_mount(self): self.reset_progress_bars() return super().did_mount() ================================================ FILE: kintree/gui/views/settings.py ================================================ import flet as ft # Common view from .common import DialogType from .common import CommonView from .common import SwitchWithRefs from .common import GUI_PARAMS from .common import handle_transition # Settings from ...config import settings as global_settings from ...config import config_interface # Load Supplier Settings supplier_settings = {} for supplier, data in global_settings.CONFIG_SUPPLIERS.items(): supplier_settings[supplier] = {} # Add enable supplier_settings[supplier]['Enable'] = [ data['enable'], ft.Switch(), None, ] # Add supplier name supplier_settings[supplier]['InvenTree Name'] = [ data['name'], ft.TextField(), None, ] # Add API fields if supplier == 'Digi-Key': digikey_api_settings = config_interface.load_file(global_settings.CONFIG_DIGIKEY_API) supplier_settings[supplier]['Client ID'] = [ digikey_api_settings['DIGIKEY_CLIENT_ID'], ft.TextField(), None, ] supplier_settings[supplier]['Client Secret'] = [ digikey_api_settings['DIGIKEY_CLIENT_SECRET'], ft.TextField(), None, ] supplier_settings[supplier]['Local Site'] = [ digikey_api_settings.get('DIGIKEY_LOCAL_SITE', 'US'), ft.TextField(), None, ] supplier_settings[supplier]['Language'] = [ digikey_api_settings.get('DIGIKEY_LOCAL_LANGUAGE', 'en'), ft.TextField(), None, ] supplier_settings[supplier]['Currency'] = [ digikey_api_settings.get('DIGIKEY_LOCAL_CURRENCY', 'USD'), ft.TextField(), None, ] elif supplier == 'Mouser': mouser_api_settings = config_interface.load_file(global_settings.CONFIG_MOUSER_API) supplier_settings[supplier]['Part API Key'] = [ mouser_api_settings['MOUSER_PART_API_KEY'], ft.TextField(), None, ] elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark': from ...search.element14_api import STORES element14_api_settings = config_interface.load_file(global_settings.CONFIG_ELEMENT14_API) default_store = element14_api_settings.get(f'{supplier.upper()}_STORE', '') supplier_settings[supplier]['Product Search API Key (Element14)'] = [ element14_api_settings['ELEMENT14_PRODUCT_SEARCH_API_KEY'], ft.TextField(), None, ] dropdown_options = [] for store_name, store_url in STORES[supplier].items(): dropdown_options.append(ft.dropdown.Option(f'{store_name} ({store_url})')) supplier_settings[supplier][f'{supplier} Store'] = [ default_store, ft.Dropdown( label='Store', width=GUI_PARAMS['dropdown_width'], dense=GUI_PARAMS['dropdown_dense'], options=dropdown_options ), None, ] elif supplier == 'LCSC': lcsc_api_settings = config_interface.load_file(global_settings.CONFIG_LCSC_API) supplier_settings[supplier]['API URL'] = [ lcsc_api_settings['LCSC_API_URL'], ft.TextField(), None, ] elif supplier == 'Jameco': jameco_api_settings = config_interface.load_file(global_settings.CONFIG_JAMECO_API) supplier_settings[supplier]['API URL'] = [ jameco_api_settings['JAMECO_API_URL'], ft.TextField(), None, ] elif supplier == 'TME': tme_api_settings = config_interface.load_file(global_settings.CONFIG_TME_API) supplier_settings[supplier]['API Token'] = [ tme_api_settings['TME_API_TOKEN'], ft.TextField(), None, ] supplier_settings[supplier]['API Secret'] = [ tme_api_settings['TME_API_SECRET'], ft.TextField(), None, ] supplier_settings[supplier]['API Country'] = [ tme_api_settings['TME_API_COUNTRY'], ft.TextField(), None, ] supplier_settings[supplier]['API Language'] = [ tme_api_settings['TME_API_LANGUAGE'], ft.TextField(), None, ] elif supplier == 'AutomationDirect': automationdirect_api_settings = config_interface.load_file(global_settings.CONFIG_AUTOMATIONDIRECT_API) supplier_settings[supplier]['API Top-Level Root Domain'] = [ automationdirect_api_settings['AUTOMATIONDIRECT_API_ROOT_URL'], ft.TextField(), None, ] supplier_settings[supplier]['API URL Path'] = [ automationdirect_api_settings['AUTOMATIONDIRECT_API_URL'], ft.TextField(), None, ] supplier_settings[supplier]['API Search Query'] = [ automationdirect_api_settings['AUTOMATIONDIRECT_API_SEARCH_QUERY'], ft.TextField(), None, ] supplier_settings[supplier]['API Search String'] = [ automationdirect_api_settings['AUTOMATIONDIRECT_API_SEARCH_STRING'], ft.TextField(), None, ] supplier_settings[supplier]['API Image Path URL'] = [ automationdirect_api_settings['AUTOMATIONDIRECT_API_IMAGE_PATH'], ft.TextField(), None, ] SETTINGS = { 'User Settings': { 'Configuration Files Folder': [ 'USER_FILES', ft.TextField(), True, # Browse enabled ], 'Cache Folder': [ 'USER_CACHE', ft.TextField(), True, # Browse enabled ], 'Save Datasheets to Local Folder': [ 'DATASHEET_SAVE_ENABLED', SwitchWithRefs(), False, # Browse enabled ], 'Datasheet Folder': [ 'DATASHEET_SAVE_PATH', ft.TextField(), True, # Browse enabled ], 'Open Browser After Creating Part': [ 'AUTOMATIC_BROWSER_OPEN', ft.Switch(), False, # Browse enabled ], 'Enable Supplier Search Cache': [ 'CACHE_ENABLED', SwitchWithRefs(), False, # Browse enabled ], 'CACHE_VALID_DAYS': [ 'CACHE_VALID_DAYS', ft.TextField( text_align=ft.TextAlign.CENTER, width=60, dense=True, disabled=True, ), False, ] }, 'Supplier Settings': supplier_settings, 'InvenTree Settings': { 'Server Address': [ 'SERVER_ADDRESS', ft.TextField(), False, # Browse disabled ], 'Username': [ 'USERNAME', ft.TextField(), False, # Browse disabled ], 'Password or Token': [ 'PASSWORD', ft.TextField(), False, # Browse disabled ], 'Enable Proxy Support': [ 'ENABLE_PROXY', SwitchWithRefs(), False, # Browse disabled ], 'Proxy': [ 'PROXY', ft.TextField(), False, # Browse disabled ], 'Upload Datasheets to InvenTree': [ 'DATASHEET_UPLOAD', SwitchWithRefs(), False, # Browse enabled ], 'Upload Pricing Data to InvenTree': [ 'PRICING_UPLOAD', SwitchWithRefs(), False, # Browse enabled ], 'Default Part Revision': [ 'INVENTREE_DEFAULT_REV', ft.TextField(), False, # Browse disabled ], 'Enable Internal Part Number (IPN)': [ 'IPN_ENABLE_CREATE', SwitchWithRefs(), False, # Browse disabled ], 'Use Manufacturer Part Number as IPN': [ 'IPN_USE_MANUFACTURER_PART_NUMBER', SwitchWithRefs(reverse_dir=True), False, # Browse disabled ], 'IPN: Enable Prefix': [ 'IPN_ENABLE_PREFIX', SwitchWithRefs(), False, # Browse disabled ], 'IPN: Prefix': [ 'IPN_PREFIX', ft.TextField(), False, # Browse disabled ], 'IPN: Enable Category Codes': [ 'IPN_CATEGORY_CODE', ft.Switch(), False, # Browse disabled ], 'IPN: Length of Unique ID': [ 'IPN_UNIQUE_ID_LENGTH', ft.TextField(), False, # Browse disabled ], 'IPN: Enable Suffix': [ 'IPN_ENABLE_SUFFIX', SwitchWithRefs(), False, # Browse disabled ], 'IPN: Suffix': [ 'IPN_SUFFIX', ft.TextField(), False, # Browse disabled ], 'Test': [ None, ft.ElevatedButton, False, # Browse disabled ], }, 'KiCad Settings': { 'Symbol Libraries Folder': [ 'KICAD_SYMBOLS_PATH', ft.TextField(), True, # Browse enabled ], 'Symbol Templates Folder': [ 'KICAD_TEMPLATES_PATH', ft.TextField(), True, # Browse enabled ], 'Footprint Libraries Folder': [ 'KICAD_FOOTPRINTS_PATH', ft.TextField(), True, # Browse enabled ], }, } # Settings AppBar settings_appbar = ft.AppBar( title=ft.WindowDragArea(ft.Container(ft.Text('Ki-nTree Settings'), width=10000), maximizable=True), bgcolor=ft.colors.SURFACE_VARIANT ) # Settings NavRail settings_navrail = ft.NavigationRail( selected_index=0, label_type=ft.NavigationRailLabelType.ALL, min_width=GUI_PARAMS['nav_rail_min_width'], min_extended_width=GUI_PARAMS['nav_rail_width'], group_alignment=GUI_PARAMS['nav_rail_alignment'], destinations=[ ft.NavigationRailDestination( label_content=ft.Text("User", size=GUI_PARAMS['nav_rail_text_size']), icon_content=ft.Icon(name=ft.icons.SUPERVISED_USER_CIRCLE, size=GUI_PARAMS['nav_rail_icon_size']), selected_icon_content=ft.Icon(name=ft.icons.SUPERVISED_USER_CIRCLE_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']), padding=GUI_PARAMS['nav_rail_padding'], ), ft.NavigationRailDestination( label_content=ft.Text("Supplier", size=GUI_PARAMS['nav_rail_text_size']), icon_content=ft.Icon(name=ft.icons.LOCAL_SHIPPING, size=GUI_PARAMS['nav_rail_icon_size']), selected_icon_content=ft.Icon(name=ft.icons.LOCAL_SHIPPING_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']), padding=GUI_PARAMS['nav_rail_padding'], ), ft.NavigationRailDestination( label_content=ft.Text("InvenTree", size=GUI_PARAMS['nav_rail_text_size']), icon_content=ft.Icon(name=ft.icons.INVENTORY_2, size=GUI_PARAMS['nav_rail_icon_size']), selected_icon_content=ft.Icon(name=ft.icons.INVENTORY_2_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']), padding=GUI_PARAMS['nav_rail_padding'], ), ft.NavigationRailDestination( label_content=ft.Text("KiCad", size=GUI_PARAMS['nav_rail_text_size']), icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT, size=GUI_PARAMS['nav_rail_icon_size']), selected_icon_content=ft.Icon(name=ft.icons.SETTINGS_INPUT_COMPONENT_OUTLINED, size=GUI_PARAMS['nav_rail_icon_size']), padding=GUI_PARAMS['nav_rail_padding'], ), ], on_change=None, ) # Navigation indexes (settings) NAV_BAR_INDEX = { 0: '/settings/user', 1: '/settings/supplier', 2: '/settings/inventree', 3: '/settings/kicad', } class SettingsView(CommonView): '''Main settings view''' title = 'Settings' route = '/settings' settings = None settings_file = None dialog = None def __init__(self, page: ft.Page): # Load setting fields self.fields = {} for field_name, field_data in SETTINGS.get(self.title, {}).items(): if isinstance(field_data, list) and field_data[0] is not None: self.fields[field_name] = field_data[1] self.fields[field_name].value = self.settings[field_data[0]] # Init view super().__init__(page=page, appbar=settings_appbar, navigation_rail=settings_navrail) if not self.appbar.actions: self.appbar.actions.extend( [ ft.IconButton( ft.icons.CLOSE, on_click=lambda _: page.window.close(), ), ] ) # Update navigation rail self.navigation_rail.on_change = self.nav_rail_redirect def nav_rail_redirect(self, e): self._page.go(NAV_BAR_INDEX[e.control.selected_index]) def save(self, settings_file=None, show_dialog=True): '''Save settings''' if settings_file is not None: settings_from_file = config_interface.load_file(settings_file) else: settings_from_file = config_interface.load_file(self.settings_file) # Update settings values for key in settings_from_file: for setting in SETTINGS[self.title].values(): if key == setting[0]: settings_from_file[key] = setting[1].value # Save if settings_file is not None: config_interface.dump_file(settings_from_file, settings_file) else: config_interface.dump_file(settings_from_file, self.settings_file) # Alert user if show_dialog: self.show_dialog( d_type=DialogType.VALID, message=f'{self.title} successfully saved', ) def on_dialog_result(self, e: ft.FilePickerResultEvent): '''Populate field with user-selected system path''' if e.path: self.fields[e.control.dialog_title].value = e.path self._page.update() def path_picker(self, e: ft.ControlEvent, title: str): '''Let user browse to a system path''' if self._page.overlay: self._page.overlay.pop() path_picker = ft.FilePicker(on_result=self.on_dialog_result) self._page.overlay.append(path_picker) self._page.update() if self.fields[title].value: path_picker.get_directory_path(dialog_title=title, initial_directory=self.fields[title].value) else: path_picker.get_directory_path(dialog_title=title, initial_directory=global_settings.HOME_DIR) def init_column(self) -> ft.Column: return ft.Column( controls=[ ft.Text(self.title, style="bodyMedium"), ft.Row(), ], alignment=ft.MainAxisAlignment.START, expand=True, ) def update_field(self, name, field, column): if isinstance(field, ft.TextField): field_predefined = bool(field.width) if not field_predefined: field.label = name field.width = GUI_PARAMS['textfield_width'] field.dense = GUI_PARAMS['textfield_dense'] if 'password' in field.label.lower(): field.password = True field_row = ft.Row( controls=[ field, ] ) # Add browse button if SETTINGS[self.title][name][2]: field_row.controls.append( ft.ElevatedButton( 'Browse', width=GUI_PARAMS['button_width'], height=48, on_click=lambda e, t=name: self.path_picker(e, title=t) ), ) column.controls.extend( [ field_row, ft.Row(height=GUI_PARAMS['textfield_space_after']), ] ) elif isinstance(field, ft.Text): field.value = name field_row = ft.Row( controls=[ field, ] ) column.controls.append(field_row) column.controls.append(ft.Divider()) elif isinstance(field, ft.TextButton): column.controls.append( ft.ElevatedButton( name, width=GUI_PARAMS['button_width'] * 2, height=GUI_PARAMS['button_height'], icon=ft.icons.CHECK_OUTLINED, on_click=lambda e, s=name: self.test_s(e, s=s) ), ) elif isinstance(field, ft.Dropdown): field.on_change = lambda _: self.save() column.controls.append( field, ) elif isinstance(field, ft.Switch) or isinstance(field, SwitchWithRefs): if 'proxy' in name.lower(): field.on_change = lambda _: None else: field.on_change = lambda _: self.save() field.label = name column.controls.append( field, ) def add_buttons(self, column, test=False) -> ft.Row: test_save_buttons = ft.Row() if test: test_save_buttons.controls.append( ft.ElevatedButton( 'Test', width=GUI_PARAMS['button_width'], height=GUI_PARAMS['button_height'], icon=ft.icons.CHECK_OUTLINED, on_click=lambda _: self.test(), ), ) test_save_buttons.controls.append( ft.ElevatedButton( 'Save', width=GUI_PARAMS['button_width'], height=GUI_PARAMS['button_height'], icon=ft.icons.SAVE_OUTLINED, on_click=lambda _: self.save() ), ) column.controls.append(test_save_buttons) def build_column(self, ignore=[]): # Header self.column = self.init_column() # Fields for name, field in self.fields.items(): if name not in ignore: self.update_field(name, field, self.column) # Test and Save buttons enable_test = bool(list(SETTINGS[self.title])[-1] == 'Test') self.add_buttons(self.column, test=enable_test) def did_mount(self): handle_transition(self._page, transition=False, timeout=0.05) return super().did_mount() class PathSettingsView(SettingsView): '''Template View for Path Setters''' def __init__(self, page: ft.Page): super().__init__(page) self.dialog = self.build_dialog() def build_dialog(self): return ft.Banner( bgcolor=ft.colors.AMBER_100, leading=ft.Icon(ft.icons.WARNING_AMBER_ROUNDED, color=ft.colors.AMBER, size=GUI_PARAMS['icon_size']), content=ft.Text(f'Restart Ki-nTree to load the new {self.title}', weight=ft.FontWeight.BOLD), actions=[ ft.TextButton('Discard', on_click=lambda _: self.show_dialog(open=False)), ], ) def show_dialog(self, d_type=None, message=None, snackbar=False, open=True): return super().show_dialog(d_type, message, snackbar, open) class UserSettingsView(PathSettingsView): '''User settings view''' title = 'User Settings' route = '/settings/user' settings = { **global_settings.USER_SETTINGS, **{ 'DATASHEET_SAVE_ENABLED': global_settings.DATASHEET_SAVE_ENABLED, 'DATASHEET_SAVE_PATH': global_settings.DATASHEET_SAVE_PATH, 'AUTOMATIC_BROWSER_OPEN': global_settings.AUTOMATIC_BROWSER_OPEN }, **{ 'CACHE_ENABLED': global_settings.CACHE_ENABLED, 'CACHE_VALID_DAYS': global_settings.CACHE_VALID_DAYS }, } settings_file_list = [ global_settings.USER_CONFIG_FILE, global_settings.CONFIG_GENERAL_PATH, global_settings.CONFIG_SEARCH_API_PATH, ] def save(self): # Save all settings for sf in self.settings_file_list: super().save(settings_file=sf, show_dialog=True) def increment_cache_value(self, inc): field = SETTINGS[self.title]['CACHE_VALID_DAYS'][1] current_value = int(field.value) if not inc: if current_value > 1: field.value = f'{current_value - 1}' else: if current_value < 99: field.value = f'{current_value + 1}' field.on_change(_=None) field.update() def build_column(self): # Header self.column = self.init_column() # Fields for name, field in self.fields.items(): self.update_field(name, field, self.column) # Create refs datasheet_row_ref = ft.Ref[ft.Row]() cache_row_ref = ft.Ref[ft.Row]() # Create row for cache validity SETTINGS[self.title]['CACHE_VALID_DAYS'][1].value = self.settings['CACHE_VALID_DAYS'] cache_row = ft.Row( ref=cache_row_ref, controls=[ ft.Text('Keep Cache Valid For (Days): '), ft.IconButton(ft.icons.REMOVE, on_click=lambda _: self.increment_cache_value(False)), SETTINGS[self.title]['CACHE_VALID_DAYS'][1], ft.IconButton(ft.icons.ADD, on_click=lambda _: self.increment_cache_value(True)), ], ) self.column.controls.append(cache_row) # Add cache row to switch refs SETTINGS[self.title]['Enable Supplier Search Cache'][1].refs = [cache_row_ref] for name, field in SETTINGS[self.title].items(): if field[0] in ['AUTOMATIC_BROWSER_OPEN', 'DATASHEET_SAVE_ENABLED', 'DATASHEET_SAVE_PATH', 'DATASHEET_INVENTREE_ENABLED']: self.fields[name].on_change = lambda _: self.save() elif field[0] in ['CACHE_ENABLED', 'CACHE_VALID_DAYS']: self.fields[name].on_change = lambda _: self.save() self.settings_file = self.settings_file_list[0] # Update datasheet ref for idx, field in enumerate(self.column.controls): if isinstance(field, SwitchWithRefs): if field.label == 'Save Datasheets to Local Folder': datasheet_row_ref.current = self.column.controls[idx + 1] SETTINGS[self.title]['Save Datasheets to Local Folder'][1].refs = [datasheet_row_ref] # Save button self.add_buttons(self.column, test=False) def did_mount(self): try: # Reset Index self.navigation_rail.selected_index = 0 self.navigation_rail.update() except AssertionError: pass return super().did_mount() class SupplierSettingsView(SettingsView): '''Supplier settings view''' title = 'Supplier Settings' route = '/settings/supplier' settings = global_settings.CONFIG_SUPPLIERS settings_file = global_settings.CONFIG_SUPPLIERS_PATH def __init__(self, page: ft.Page): super().__init__(page) def save_s(self, e: ft.ControlEvent, supplier: str, show_dialog=True): '''Save supplier settings''' # Enable/Name settings supplier_settings = self.settings enable_name = { 'enable': SETTINGS[self.title][supplier]['Enable'][1].value, 'name': SETTINGS[self.title][supplier]['InvenTree Name'][1].value, } supplier_settings.update({supplier: enable_name}) config_interface.dump_file(supplier_settings, self.settings_file) # Update suppliers global_settings.load_suppliers() # API settings if supplier == 'Digi-Key': from ...search import digikey_api # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_DIGIKEY_API) # Update settings values updated_settings = { 'DIGIKEY_CLIENT_ID': SETTINGS[self.title][supplier]['Client ID'][1].value, 'DIGIKEY_CLIENT_SECRET': SETTINGS[self.title][supplier]['Client Secret'][1].value, 'DIGIKEY_LOCAL_SITE': SETTINGS[self.title][supplier]['Local Site'][1].value, 'DIGIKEY_LOCAL_LANGUAGE': SETTINGS[self.title][supplier]['Language'][1].value, 'DIGIKEY_LOCAL_CURRENCY': SETTINGS[self.title][supplier]['Currency'][1].value, } digikey_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(digikey_settings, global_settings.CONFIG_DIGIKEY_API) digikey_api.setup_environment(force=True) elif supplier == 'Mouser': from ...search import mouser_api # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_MOUSER_API) # Update settings values updated_settings = { 'MOUSER_PART_API_KEY': SETTINGS[self.title][supplier]['Part API Key'][1].value, } mouser_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(mouser_settings, global_settings.CONFIG_MOUSER_API) mouser_api.setup_environment(force=True) elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark': # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_ELEMENT14_API) # Update settings values updated_settings = { 'ELEMENT14_PRODUCT_SEARCH_API_KEY': SETTINGS[self.title][supplier]['Product Search API Key (Element14)'][1].value, f'{supplier.upper()}_STORE': SETTINGS[self.title][supplier][f'{supplier} Store'][1].value, } element14_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(element14_settings, global_settings.CONFIG_ELEMENT14_API) elif supplier == 'LCSC': # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_LCSC_API) # Update settings values updated_settings = { 'LCSC_API_URL': SETTINGS[self.title][supplier]['API URL'][1].value, } lcsc_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(lcsc_settings, global_settings.CONFIG_LCSC_API) elif supplier == 'Jameco': # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_JAMECO_API) # Update settings values updated_settings = { 'JAMECO_API_URL': SETTINGS[self.title][supplier]['API URL'][1].value, } jameco_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(jameco_settings, global_settings.CONFIG_JAMECO_API) elif supplier == 'TME': # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_TME_API) # Update settings values updated_settings = { 'TME_API_TOKEN': SETTINGS[self.title][supplier]['API Token'][1].value, 'TME_API_SECRET': SETTINGS[self.title][supplier]['API Secret'][1].value, 'TME_API_COUNTRY': SETTINGS[self.title][supplier]['API Country'][1].value, 'TME_API_LANGUAGE': SETTINGS[self.title][supplier]['API Language'][1].value, } tme_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(tme_settings, global_settings.CONFIG_TME_API) elif supplier == 'AutomationDirect': # Load settings from file settings_from_file = config_interface.load_file(global_settings.CONFIG_AUTOMATIONDIRECT_API) # Update settings values updated_settings = { 'AUTOMATIONDIRECT_API_ROOT_URL': SETTINGS[self.title][supplier]['API Top-Level Root Domain'][1].value, 'AUTOMATIONDIRECT_API_URL': SETTINGS[self.title][supplier]['API URL Path'][1].value, 'AUTOMATIONDIRECT_API_SEARCH_QUERY': SETTINGS[self.title][supplier]['API Search Query'][1].value, 'AUTOMATIONDIRECT_API_SEARCH_STRING': SETTINGS[self.title][supplier]['API Search String'][1].value, 'AUTOMATIONDIRECT_API_IMAGE_PATH': SETTINGS[self.title][supplier]['API Image Path URL'][1].value, } automationdirect_settings = {**settings_from_file, **updated_settings} config_interface.dump_file(automationdirect_settings, global_settings.CONFIG_AUTOMATIONDIRECT_API) if show_dialog: self.show_dialog( d_type=DialogType.VALID, message=f'{supplier} Settings successfully saved', ) def test_s(self, e: ft.ControlEvent, supplier: str): '''Test supplier API settings''' self.save_s(e, supplier, show_dialog=False) result = False if supplier == 'Digi-Key': from ...search import digikey_api result = digikey_api.test_api() elif supplier == 'Mouser': from ...search import mouser_api result = mouser_api.test_api() elif supplier == 'Element14' or supplier == 'Farnell' or supplier == 'Newark': from ...search import element14_api result = element14_api.test_api() elif supplier == 'LCSC': from ...search import lcsc_api result = lcsc_api.test_api() elif supplier == 'TME': from ...search import tme_api result = tme_api.test_api() elif supplier == 'Jameco': from ...search import jameco_api result = jameco_api.test_api() elif supplier == 'AutomationDirect': from ...search import automationdirect_api result = automationdirect_api.test_api() if result: self.show_dialog( d_type=DialogType.VALID, message=f'Successfully connected to {supplier} API' ) else: self.show_dialog( d_type=DialogType.ERROR, message=f'ERROR: Failed to connect to {supplier} API. Verify the {supplier} credentials and re-try' ) def build_column(self): # Header self.column = self.init_column() # Tabs supplier_tabs = ft.Tabs( selected_index=0, animation_duration=10, expand=1, tabs=[], ) for supplier, settings in SETTINGS[self.title].items(): supplier_tab_content = [ ft.Row(height=10), ] for setting_name, setting_data in settings.items(): setting_data[1].label = setting_name setting_data[1].width = GUI_PARAMS['textfield_width'] setting_data[1].dense = GUI_PARAMS['textfield_dense'] setting_data[1].value = setting_data[0] supplier_tab_content.extend( [ ft.Row([setting_data[1]]), ft.Row(height=GUI_PARAMS['textfield_space_after']), ] ) # Test and Save buttons supplier_tab_content.append( ft.Row( controls=[ ft.ElevatedButton( 'Test', width=GUI_PARAMS['button_width'], height=GUI_PARAMS['button_height'], icon=ft.icons.CHECK_OUTLINED, on_click=lambda e, s=supplier: self.test_s(e, supplier=s), ), ft.ElevatedButton( 'Save', width=GUI_PARAMS['button_width'], height=GUI_PARAMS['button_height'], icon=ft.icons.SAVE_OUTLINED, on_click=lambda e, s=supplier: self.save_s(e, supplier=s), ), ] ) ) supplier_tabs.tabs.append( ft.Tab( tab_content=ft.Text(supplier, size=16), content=ft.Container( ft.Column( controls=supplier_tab_content, ) ) ) ) self.column.controls.append(supplier_tabs) class InvenTreeSettingsView(SettingsView): '''InvenTree settings view''' title = 'InvenTree Settings' route = '/settings/inventree' settings_file = [ global_settings.INVENTREE_CONFIG, global_settings.CONFIG_IPN_PATH, ] def save(self, file=None, dialog=True): address = SETTINGS[self.title]['Server Address'][1].value proxy = SETTINGS[self.title]['Proxy'][1].value enable_proxy = SETTINGS[self.title]['Enable Proxy Support'][1].value if not enable_proxy: proxies = None elif address.startswith('https'): proxies = {'https': proxy} else: proxies = {'http': proxy} if file is None: # Save to InvenTree file config_interface.save_inventree_user_settings( enable=global_settings.ENABLE_INVENTREE, server=address, username=SETTINGS[self.title]['Username'][1].value, password=SETTINGS[self.title]['Password or Token'][1].value, enable_proxy=enable_proxy, proxies=proxies, datasheet_upload=SETTINGS[self.title][ 'Upload Datasheets to InvenTree'][1].value, pricing_upload=SETTINGS[self.title][ 'Upload Pricing Data to InvenTree'][1].value, user_config_path=self.settings_file[0] ) # Alert user if dialog: self.show_dialog( d_type=DialogType.VALID, message=f'{self.title} successfully saved', ) else: super().save(settings_file=file, show_dialog=dialog) # Reload InvenTree settings global_settings.load_inventree_settings() # Reload IPN settings global_settings.load_ipn_settings() def test(self): from ...database import inventree_interface self.save(dialog=False) connection = inventree_interface.connect_to_server() if connection: self.show_dialog( d_type=DialogType.VALID, message='Sucessfully connected to InvenTree server', ) else: self.show_dialog( d_type=DialogType.ERROR, message='Failed to connect to InvenTree server. Check InvenTree credentials are correct and server is running', ) def __init__(self, page: ft.Page): # Load InvenTree and IPN settings self.settings = { **config_interface.load_inventree_user_settings(self.settings_file[0]), **config_interface.load_file(self.settings_file[1]), } super().__init__(page) def build_column(self): ipn_file = self.settings_file[1] ipn_fields = [ 'Default Part Revision', 'Enable Internal Part Number (IPN)', 'Use Manufacturer Part Number as IPN', 'IPN: Enable Prefix', 'IPN: Prefix', 'IPN: Enable Category Codes', 'IPN: Length of Unique ID', 'IPN: Enable Suffix', 'IPN: Suffix', ] # Tabs inventree_tabs = ft.Tabs( selected_index=0, animation_duration=10, expand=1, tabs=[], ) # Build server tab content server_col = ft.Column([ft.Row(height=10)]) for name, field in self.fields.items(): if name not in ipn_fields: self.update_field(name, field, server_col) self.add_buttons(server_col, test=True) # Add InvenTree server tab inventree_tabs.tabs.append( ft.Tab( tab_content=ft.Text('Server', size=16), content=ft.Container( server_col, ) ) ) # Link Proxy Switch to the input field ref = ft.Ref[ft.TextField]() ref.current = SETTINGS[self.title]['Proxy'][1] SETTINGS[self.title]['Enable Proxy Support'][1].refs = [ref] # Create IPN fields ipn_fields_ref = ft.Ref[ft.Row]() ipn_fields_col = ft.Column( ref=ipn_fields_ref, controls=[], ) for name in ipn_fields: SETTINGS[self.title][name][1].label = name SETTINGS[self.title][name][1].on_change = lambda _: self.save( file=ipn_file, dialog=False, ) if name.startswith('IPN: '): ipn_fields_col.controls.append( ft.Row([SETTINGS[self.title][name][1]]) ) ipn_manufacturer_part_number_ref = ft.Ref[ft.Row]() ipn_manufacturer_part_number_col = ft.Column( ref=ipn_manufacturer_part_number_ref, controls=[ ft.Row([SETTINGS[self.title]['Use Manufacturer Part Number as IPN'][1]]), ft.Row([ipn_fields_col]), ], ) # Build IPN tab column ipn_tab_col = ft.Column( [ ft.Row(height=10), ft.Row([SETTINGS[self.title]['Default Part Revision'][1]]), ft.Row([SETTINGS[self.title]['Enable Internal Part Number (IPN)'][1]]), ft.Row([ipn_manufacturer_part_number_col]), ] ) # Link main IPN switch to corresponding fields main_control = 'Enable Internal Part Number (IPN)' secondary_control = 'Use Manufacturer Part Number as IPN' SETTINGS[self.title][main_control][1].refs = [ipn_manufacturer_part_number_ref] SETTINGS[self.title][main_control][1].on_change = lambda _: self.save( file=ipn_file, dialog=False, ) # Link Manufacturer Part Number switch to corresponding fields SETTINGS[self.title][secondary_control][1].refs = [ipn_fields_ref] SETTINGS[self.title][secondary_control][1].on_change = lambda _: self.save( file=ipn_file, dialog=False, ) # Link prefix/suffix switches to corresponding fields for name in ['IPN: Enable Prefix', 'IPN: Enable Suffix']: ref = ft.Ref[ft.TextField]() ref.current = SETTINGS[self.title][name.replace('Enable ', '')][1] SETTINGS[self.title][name][1].refs = [ref] # Add IPN tab inventree_tabs.tabs.append( ft.Tab( tab_content=ft.Text('Internal Part Number', size=16), content=ft.Container( ipn_tab_col, ) ) ) # Build column self.column = self.init_column() # Add tabs self.column.controls.append(inventree_tabs) class KiCadSettingsView(PathSettingsView): '''KiCad settings view''' title = 'KiCad Settings' route = '/settings/kicad' settings = global_settings.KICAD_SETTINGS settings_file = global_settings.KICAD_CONFIG_PATHS ================================================ FILE: kintree/kicad/kicad_interface.py ================================================ from . import kicad_symbol def inventree_to_kicad(part_data: dict, library_path: str, template_path=None, show_progress=True) -> bool: ''' Create KiCad symbol from InvenTree part data ''' klib = kicad_symbol.ComponentLibManager(library_path) return klib.add_symbol_to_library_from_inventree( symbol_data=part_data, template_path=template_path, show_progress=show_progress ) ================================================ FILE: kintree/kicad/kicad_symbol.py ================================================ import os from ..config import settings from ..common import progress from ..common.tools import cprint from kiutils.symbol import SymbolLib # KiCad Component Library Manager class ComponentLibManager(object): def __init__(self, library_path): # Load library and template paths cprint(f'[KCAD]\tlibrary_path: {library_path}', silent=settings.SILENT) # Check files exist if not os.path.isfile(library_path): cprint(f'[KCAD]\tError loading library file ({library_path})', silent=settings.SILENT) return None # Load library self.kicad_lib = SymbolLib.from_file(library_path) self.library_name = library_path.split(os.sep)[-1] cprint('[KCAD]\tNumber of parts in library ' + self.library_name + ': ' + str(len(self.kicad_lib.symbols)), silent=settings.SILENT) def is_symbol_in_library(self, symbol_id): ''' Check if symbol already exists in library ''' for symbol in self.kicad_lib.symbols: cprint(f'[DBUG]\t{symbol.libId} ?= {symbol_id}', silent=settings.HIDE_DEBUG) if symbol.libId == symbol_id: cprint(f'[KCAD]\tWarning: Component {symbol_id} already in library', silent=settings.SILENT) return True return False def add_symbol_to_library_from_inventree(self, symbol_data, template_path=None, show_progress=True): ''' Create symbol in KiCad library ''' part_in_lib = False new_part = False part_name = '' parameters = symbol_data.get('parameters', {}) parameters = {**symbol_data, **parameters} key_list = list(parameters.keys()) key_list.sort(key=len, reverse=True) def replace_wildcards(field): for key in key_list: if key in field: field = field.replace(key, parameters[key]) return field symbol_id = symbol_data.get('Symbol', '').split(':') if not symbol_id: cprint('[KCAD] Error: Adding a new symbol to a KiCad library requires the \'Symbol\' key with the following format: {lib}:{symbol_id}') return part_in_lib, new_part, part_name if not template_path: category = symbol_data['Template'][0] subcategory = symbol_data['Template'][1] # Fetch template path try: template_path = settings.symbol_templates_paths[category][subcategory] except: template_path = settings.symbol_templates_paths[category]['Default'] # Check files exist if not self.kicad_lib: return part_in_lib, new_part if not os.path.isfile(template_path): cprint(f'[KCAD]\tError loading template file ({template_path})', silent=settings.SILENT) return part_in_lib, new_part, part_name # Load template templatelib = SymbolLib.from_file(template_path) # Load new symbol if len(templatelib.symbols) == 1: for symbol in templatelib.symbols: new_symbol = symbol else: cprint('[KCAD]\tError: Found more than 1 symbol template in template file, aborting', silent=settings.SILENT) return part_in_lib, new_part, part_name # Update name/ID part_name = replace_wildcards(new_symbol.libId) new_symbol.libId = part_name # Check if part already in library try: is_symbol_in_library = self.is_symbol_in_library(part_name) part_in_lib = True except: is_symbol_in_library = False if is_symbol_in_library: return part_in_lib, new_part, part_name # Progress Update if not progress.update_progress_bar(show_progress): return part_in_lib, new_part, part_name # Update properties for property in new_symbol.properties: property.value = replace_wildcards(property.value) # Add symbol to library self.kicad_lib.symbols.append(new_symbol) # Write library self.kicad_lib.to_file(encoding="utf-8") cprint(f'[KCAD]\tSuccess: Component added to library {self.library_name}', silent=settings.SILENT) part_in_lib = True new_part = True # Progress Update if not progress.update_progress_bar(show_progress): pass return part_in_lib, new_part, part_name ================================================ FILE: kintree/kicad/templates/LICENSE ================================================ Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. ================================================ FILE: kintree/kicad/templates/capacitor-polarized.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Capacitance (Farad)" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "Tolerance" (at 5.08 -3.81 0) (effects (font (size 1.27 1.27)) (justify left) hide) ) (property "Voltage Rated (Volt)" "Rated Voltage" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Package Type" "Package Type" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) ) (property "Package Size" "Package Size" (at 0 -8.89 0) (effects (font (size 1.27 1.27)) hide) ) (property "ESR (Ohm)" "ESR" (at 5.08 -6.35 0) (effects (font (size 1.27 1.27)) (justify left) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy -1.778 -0.381) (xy -1.778 -1.651) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 0) (xy -0.635 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -1.143 -1.016) (xy -2.413 -1.016) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -0.635 -1.905) (xy -0.635 1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.524 0) (xy 0.635 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (arc (start 1.524 1.905) (mid 0.8343 0) (end 1.524 -1.905) (stroke (width 0.254) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -3.81 0 0) (length 2.54) (name "Positif" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 3.81 0 180) (length 2.54) (name "Negatif" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/capacitor.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Capacitance (Farad)" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "Tolerance" (at 7.62 -3.81 0) (effects (font (size 1.27 1.27)) hide) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Temperature Grade" "Temperature Grade" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Package Type" "Package Type" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy -1.27 0) (xy -1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -0.889 1.905) (xy -0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 0.889 1.905) (xy 0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.905 0) (xy 2.54 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -3.81 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 3.81 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/connector.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "J" (at 0 0 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -5.08 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) ) ) ================================================ FILE: kintree/kicad/templates/crystal-2p.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "Y" (at 0 5.08 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -22.86 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -7.62 0) (effects (font (size 1.27 1.27))) ) (property "Frequency" "Value" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) ) (property "Load Capacitance (Farad)" "Load Capacitance" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Package Size" "Package Size" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_1_1" (rectangle (start -1.016 3.048) (end 1.016 -3.048) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy -2.54 0) (xy -1.778 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 2.54 0) (xy 1.778 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -1.778 2.54) (xy -1.778 0) (xy -1.778 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 2.54) (xy 1.778 0) (xy 1.778 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (pin passive line (at -5.08 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 5.08 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/default.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "DES" (at 0 0 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -5.08 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) ) ) ================================================ FILE: kintree/kicad/templates/diode-led.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "D" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -22.86 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Forward Voltage (Volt)" "Forward Voltage" (at 0 -7.62 0) (effects (font (size 1.27 1.27))) ) (property "LED Color" "LED Color" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -1.27) (xy 1.27 1.27) ) (stroke (width 0.381) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 1.27) (xy -1.27 -1.27) (xy 1.27 0) (xy -1.27 1.27) ) (stroke (width 0.254) (type default)) (fill (type background)) ) (polyline (pts (xy -0.889 -1.778) (xy 0.508 -3.175) (xy 0.254 -2.413) (xy -0.254 -2.921) (xy 0.508 -3.175) ) (stroke (width 0.254) (type default)) (fill (type outline)) ) (polyline (pts (xy 0.635 -1.651) (xy 2.032 -3.048) (xy 1.27 -2.794) (xy 1.778 -2.286) (xy 2.032 -3.048) ) (stroke (width 0.254) (type default)) (fill (type outline)) ) ) (symbol "IPN_1_1" (pin passive line (at 3.81 0 180) (length 2.54) (name "Cathode" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -3.81 0 0) (length 2.54) (name "Anode" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/diode-schottky.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "D" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Forward Voltage (Volt)" "Forward Voltage" (at 0 -8.89 0) (effects (font (size 1.27 1.27)) hide) ) (property "Current (Amps)" "Rated Current" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -1.397) (xy 2.159 -1.397) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 -1.397) (xy 1.27 1.397) (xy 0.381 1.397) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 1.27) (xy -1.27 -1.27) (xy 1.27 0) (xy -1.27 1.27) ) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "IPN_1_1" (pin passive line (at 3.81 0 180) (length 2.54) (name "Cathode" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -3.81 0 0) (length 2.54) (name "Anode" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/diode-standard.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "D" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Forward Voltage (Volt)" "Forward Voltage" (at 0 -8.89 0) (effects (font (size 1.27 1.27)) hide) ) (property "Current (Amps)" "Rated Current" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -1.397) (xy 1.27 1.397) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 1.27) (xy -1.27 -1.27) (xy 1.27 0) (xy -1.27 1.27) ) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "IPN_1_1" (pin passive line (at 3.81 0 180) (length 2.54) (name "Cathode" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -3.81 0 0) (length 2.54) (name "Anode" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/diode-zener.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "D" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Forward Voltage (Volt)" "Forward Voltage" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Power (Watts)" "Rated Power" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -1.397) (xy 2.159 -1.778) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 -1.397) (xy 1.27 1.397) (xy 0.381 1.778) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 1.27) (xy -1.27 -1.27) (xy 1.27 0) (xy -1.27 1.27) ) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "IPN_1_1" (pin passive line (at 3.81 0 180) (length 2.54) (name "Cathode" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -3.81 0 0) (length 2.54) (name "Anode" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/eeprom-sot23.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (in_bom yes) (on_board yes) (property "Reference" "U" (at 0 8.89 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (rectangle (start -5.08 7.62) (end 5.08 -7.62) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "IPN_1_1" (pin passive line (at -7.62 0 0) (length 2.54) (name "SCL" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin power_in line (at 7.62 -5.08 180) (length 2.54) (name "VSS" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -7.62 -5.08 0) (length 2.54) (name "SDA" (effects (font (size 1.27 1.27)))) (number "3" (effects (font (size 1.27 1.27)))) ) (pin power_in line (at -7.62 5.08 0) (length 2.54) (name "VCC" (effects (font (size 1.27 1.27)))) (number "4" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 7.62 5.08 180) (length 2.54) (name "WP" (effects (font (size 1.27 1.27)))) (number "5" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/ferrite-bead.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "FB" (at 0 2.54 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Inductance (Henry)" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "Current Rating (Ampere)" "Rated Current" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) ) (property "ESR (Ohm)" "ESR" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy -4.318 0) (xy -4.064 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 0) (xy 1.27 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 4.064 0) (xy 4.318 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -2.54 1.27) (xy -2.54 -1.27) (xy 2.54 -1.27) (xy 2.54 1.27) (xy -2.54 1.27) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -5.08 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 5.08 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/fuse.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "F" (at 0 2.794 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Current Rating (A)" "Rated Current" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "Voltage Rating (V)" "Rated Voltage" (at 0 -5.08 0) (effects (font (size 1.27 1.27)) hide) ) (property "Package Type" "Package Type" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (rectangle (start -3.81 0) (end -3.048 0) (stroke (width 0) (type default)) (fill (type none)) ) (arc (start 0 0) (mid 1.524 -1.5174) (end 3.048 0) (stroke (width 0) (type default)) (fill (type none)) ) (rectangle (start 3.048 0) (end 3.81 0) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (arc (start 0 0) (mid -1.524 1.5174) (end -3.048 0) (stroke (width 0) (type default)) (fill (type none)) ) (pin passive line (at -5.08 0 0) (length 1.27) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 5.08 0 180) (length 1.27) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/inductor.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "L" (at 0 2.54 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -8.89 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Inductance (Henry)" "Value" (at 0 -1.27 0) (effects (font (size 1.27 1.27))) ) (property "Current Rating (Ampere)" "Rated Current" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "ESR (Ohm)" "ESR" (at 0 -6.35 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (arc (start -1.905 0) (mid -2.8575 0.9399) (end -3.81 0) (stroke (width 0) (type default)) (fill (type none)) ) (arc (start 0 0) (mid -0.9525 0.9399) (end -1.905 0) (stroke (width 0) (type default)) (fill (type none)) ) (arc (start 1.905 0) (mid 0.9525 0.9399) (end 0 0) (stroke (width 0) (type default)) (fill (type none)) ) (arc (start 3.81 0) (mid 2.8575 0.9399) (end 1.905 0) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -6.35 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 6.35 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/integrated-circuit.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "U" (at 0 0 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -5.08 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) ) ) ================================================ FILE: kintree/kicad/templates/library_template.kicad_sym ================================================ (kicad_symbol_lib (version 20211014) (generator kicad_converter)) ================================================ FILE: kintree/kicad/templates/oscillator-4p.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (in_bom yes) (on_board yes) (property "Reference" "Y" (at 0 6.35 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) ) (property "Part Number" "Value" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "Package Size" "Package Size" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_1_1" (rectangle (start -5.08 5.08) (end 5.08 -5.08) (stroke (width 0) (type default)) (fill (type background)) ) (pin input line (at -10.16 -2.54 0) (length 5.08) (name "OE" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin power_out line (at 10.16 -2.54 180) (length 5.08) (name "GND" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) (pin output line (at 10.16 2.54 180) (length 5.08) (name "OUT" (effects (font (size 1.27 1.27)))) (number "3" (effects (font (size 1.27 1.27)))) ) (pin power_in line (at -10.16 2.54 0) (length 5.08) (name "VDD" (effects (font (size 1.27 1.27)))) (number "4" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/protection-unidir.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "D" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -13.97 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -21.59 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -24.13 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -16.51 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -19.05 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Standoff Voltage" "Standoff Voltage" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Breakdown Voltage" "Breakdown Voltage" (at 0 -8.89 0) (effects (font (size 1.27 1.27)) hide) ) (property "Peak Power (Watts)" "Rated Power" (at 0 -11.43 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -1.397) (xy 2.159 -1.778) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 -1.397) (xy 1.27 1.397) (xy 0.381 1.778) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy -1.27 1.27) (xy -1.27 -1.27) (xy 1.27 0) (xy -1.27 1.27) ) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "IPN_1_1" (pin passive line (at 3.81 0 180) (length 2.54) (name "Cathode" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -3.81 0 0) (length 2.54) (name "Anode" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/resistor-sm.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "R" (at 0 2.032 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Resistance (Ohms)" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "Tolerance" (at 3.81 -2.54 0) (effects (font (size 1.27 1.27)) (justify left) hide) ) (property "Package Type" "Package Type" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) ) (property "Power (Watts)" "Rated Power" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy -3.81 0) (xy -3.048 0) (xy -2.54 0.762) (xy -1.524 -0.762) (xy -0.508 0.762) (xy 0.508 -0.762) (xy 1.524 0.762) (xy 2.54 -0.762) (xy 3.048 0) (xy 3.81 0) (xy 3.81 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -5.08 0 0) (length 1.27) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 5.08 0 180) (length 1.27) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/resistor.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "R" (at 0 2.032 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Resistance (Ohms)" "Value" (at 0 -2.54 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "Tolerance" (at 3.81 -2.54 0) (effects (font (size 1.27 1.27)) (justify left) hide) ) (property "Package Type" "Package Type" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) ) (property "Power (Watts)" "Rated Power" (at 0 -7.62 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy -3.81 0) (xy -3.048 0) (xy -2.54 0.762) (xy -1.524 -0.762) (xy -0.508 0.762) (xy 0.508 -0.762) (xy 1.524 0.762) (xy 2.54 -0.762) (xy 3.048 0) (xy 3.81 0) (xy 3.81 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "IPN_1_1" (pin passive line (at -5.08 0 0) (length 1.27) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 5.08 0 180) (length 1.27) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/transistor-nfet.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "Q" (at 0 4.318 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 7.62 3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 7.62 1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Current (Amps)" "Rated Current" (at 7.62 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Package Type" "Package Type" (at 7.62 -3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 0) (xy 1.27 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.27 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -1.27) (xy 1.778 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -0.635) (xy 1.778 0.635) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 1.27) (xy 1.778 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 -2.54) (xy 3.81 -1.905) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 4.953 0.508) (xy 6.223 0.508) ) (stroke (width 0.127) (type default)) (fill (type none)) ) (polyline (pts (xy 5.08 -0.635) (xy 4.953 -0.635) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 1.905) (xy 3.81 1.905) (xy 3.81 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 5.588 -0.635) (xy 5.588 -2.413) (xy 3.81 -2.413) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 5.588 0.635) (xy 5.588 2.286) (xy 3.81 2.286) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -1.905) (xy 3.81 -1.905) (xy 3.81 0) (xy 3.048 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.905 0) (xy 3.048 0.635) (xy 3.048 -0.635) (xy 1.905 0) ) (stroke (width 0) (type default)) (fill (type outline)) ) (polyline (pts (xy 4.953 -0.635) (xy 5.588 0.508) (xy 6.223 -0.635) (xy 5.08 -0.635) ) (stroke (width 0) (type default)) (fill (type outline)) ) ) (symbol "IPN_1_1" (pin bidirectional line (at 3.81 7.62 270) (length 5.08) (name "D" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin input line (at -3.81 0 0) (length 5.08) (name "G" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin bidirectional line (at 3.81 -7.62 90) (length 5.08) (name "S" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/transistor-npn.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "Q" (at 0 4.318 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 5.08 3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 5.08 1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Current (Amps)" "Rated Current" (at 5.08 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Package Type" "Package Type" (at 5.08 -3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 2.286) (xy 1.27 -2.286) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 -2.54) (xy 1.27 -1.016) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 2.54) (xy 1.27 1.016) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 -2.54) (xy 3.175 -1.27) (xy 2.413 -2.413) (xy 3.81 -2.54) ) (stroke (width 0) (type default)) (fill (type outline)) ) ) (symbol "IPN_1_1" (pin input line (at -3.81 0 0) (length 5.08) (name "B" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin input line (at 3.81 7.62 270) (length 5.08) (name "C" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin output line (at 3.81 -7.62 90) (length 5.08) (name "E" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/transistor-pfet.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "Q" (at 0 4.318 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 7.62 3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 7.62 1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Current (Amps)" "Rated Current" (at 7.62 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Package Type" "Package Type" (at 7.62 -3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 0) (xy 1.27 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.27 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -1.905) (xy 3.81 -1.905) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -1.27) (xy 1.778 -2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 -0.635) (xy 1.778 0.635) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 0) (xy 2.667 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 1.27) (xy 1.778 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 -2.54) (xy 3.81 -1.905) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 4.953 -0.635) (xy 6.223 -0.635) ) (stroke (width 0.127) (type default)) (fill (type none)) ) (polyline (pts (xy 5.08 0.508) (xy 4.953 0.508) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.778 1.905) (xy 3.81 1.905) (xy 3.81 2.54) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.683 0) (xy 3.81 0) (xy 3.81 -1.905) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 5.588 -0.635) (xy 5.588 -2.413) (xy 3.81 -2.413) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 5.588 0.508) (xy 5.588 2.286) (xy 3.81 2.286) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.683 0) (xy 2.667 0.762) (xy 2.667 -0.762) (xy 3.683 0) ) (stroke (width 0) (type default)) (fill (type outline)) ) (polyline (pts (xy 4.953 0.508) (xy 5.588 -0.635) (xy 6.223 0.508) (xy 5.08 0.508) ) (stroke (width 0) (type default)) (fill (type outline)) ) ) (symbol "IPN_1_1" (pin bidirectional line (at 3.81 7.62 270) (length 5.08) (name "D" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin input line (at -3.81 0 0) (length 5.08) (name "G" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin bidirectional line (at 3.81 -7.62 90) (length 5.08) (name "S" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates/transistor-pnp.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "IPN" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "Q" (at 0 4.318 0) (effects (font (size 1.27 1.27))) ) (property "Value" "IPN" (at 0 -10.16 0) (effects (font (size 1.27 1.27)) hide) ) (property "Footprint" "Footprint" (at 0 -17.78 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "inventree_url" (at 0 -20.32 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer" "Manufacturer" (at 0 -12.7 0) (effects (font (size 1.27 1.27)) hide) ) (property "Manufacturer Part Number" "MPN" (at 0 -15.24 0) (effects (font (size 1.27 1.27)) hide) ) (property "Part Number" "Value" (at 5.08 3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Voltage (Volt)" "Rated Voltage" (at 5.08 1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Rated Current (Amps)" "Rated Current" (at 5.08 -1.27 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "Package Type" "Package Type" (at 5.08 -3.81 0) (effects (font (size 1.27 1.27)) (justify left)) ) (property "ki_keywords" "keywords" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "description" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "IPN_0_1" (polyline (pts (xy 1.27 -2.286) (xy 1.27 2.286) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 -2.54) (xy 1.27 -1.016) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 3.81 2.54) (xy 1.27 1.016) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.397 -1.016) (xy 2.032 -2.286) (xy 2.794 -1.143) (xy 1.397 -1.016) ) (stroke (width 0) (type default)) (fill (type outline)) ) ) (symbol "IPN_1_1" (pin input line (at -3.81 0 0) (length 5.08) (name "B" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin output line (at 3.81 7.62 270) (length 5.08) (name "C" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) (pin input line (at 3.81 -7.62 90) (length 5.08) (name "E" (effects (font (size 1.27 1.27)))) (number "~" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: kintree/kicad/templates_project/templates_project.kicad_pcb ================================================ (kicad_pcb (version 20221018) (generator pcbnew) (general (thickness 1.6) ) (paper "A4") (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (32 "B.Adhes" user "B.Adhesive") (33 "F.Adhes" user "F.Adhesive") (34 "B.Paste" user) (35 "F.Paste" user) (36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen") (38 "B.Mask" user) (39 "F.Mask" user) (40 "Dwgs.User" user "User.Drawings") (41 "Cmts.User" user "User.Comments") (42 "Eco1.User" user "User.Eco1") (43 "Eco2.User" user "User.Eco2") (44 "Edge.Cuts" user) (45 "Margin" user) (46 "B.CrtYd" user "B.Courtyard") (47 "F.CrtYd" user "F.Courtyard") (48 "B.Fab" user) (49 "F.Fab" user) (50 "User.1" user) (51 "User.2" user) (52 "User.3" user) (53 "User.4" user) (54 "User.5" user) (55 "User.6" user) (56 "User.7" user) (57 "User.8" user) (58 "User.9" user) ) (setup (pad_to_mask_clearance 0) (pcbplotparams (layerselection 0x00010fc_ffffffff) (plot_on_all_layers_selection 0x0000000_00000000) (disableapertmacros false) (usegerberextensions false) (usegerberattributes true) (usegerberadvancedattributes true) (creategerberjobfile true) (dashed_line_dash_ratio 12.000000) (dashed_line_gap_ratio 3.000000) (svgprecision 4) (plotframeref false) (viasonmask false) (mode 1) (useauxorigin false) (hpglpennumber 1) (hpglpenspeed 20) (hpglpendiameter 15.000000) (dxfpolygonmode true) (dxfimperialunits true) (dxfusepcbnewfont true) (psnegative false) (psa4output false) (plotreference true) (plotvalue true) (plotinvisibletext false) (sketchpadsonfab false) (subtractmaskfromsilk false) (outputformat 1) (mirror false) (drillshape 1) (scaleselection 1) (outputdirectory "") ) ) (net 0 "") ) ================================================ FILE: kintree/kicad/templates_project/templates_project.kicad_prl ================================================ { "board": { "active_layer": 0, "active_layer_preset": "", "auto_track_width": true, "hidden_netclasses": [], "hidden_nets": [], "high_contrast_mode": 0, "net_color_mode": 1, "opacity": { "images": 0.6, "pads": 1.0, "tracks": 1.0, "vias": 1.0, "zones": 0.6 }, "ratsnest_display_mode": 0, "selection_filter": { "dimensions": true, "footprints": true, "graphics": true, "keepouts": true, "lockedItems": true, "otherItems": true, "pads": true, "text": true, "tracks": true, "vias": true, "zones": true }, "visible_items": [ 0, 1, 2, 3, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36 ], "visible_layers": "fffffff_ffffffff", "zone_display_mode": 0 }, "meta": { "filename": "templates_project.kicad_prl", "version": 3 }, "project": { "files": [] } } ================================================ FILE: kintree/kicad/templates_project/templates_project.kicad_pro ================================================ { "board": { "3dviewports": [], "design_settings": { "defaults": { "board_outline_line_width": 0.1, "copper_line_width": 0.2, "copper_text_size_h": 1.5, "copper_text_size_v": 1.5, "copper_text_thickness": 0.3, "other_line_width": 0.15, "silk_line_width": 0.15, "silk_text_size_h": 1.0, "silk_text_size_v": 1.0, "silk_text_thickness": 0.15 }, "diff_pair_dimensions": [], "drc_exclusions": [], "rules": { "solder_mask_clearance": 0.0, "solder_mask_min_width": 0.0 }, "track_widths": [], "via_dimensions": [] }, "layer_presets": [], "viewports": [] }, "boards": [], "cvpcb": { "equivalence_files": [] }, "erc": { "erc_exclusions": [], "meta": { "version": 0 }, "pin_map": [ [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2 ], [ 0, 2, 0, 1, 0, 0, 1, 0, 2, 2, 2, 2 ], [ 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 2 ], [ 0, 1, 0, 0, 0, 0, 1, 1, 2, 1, 1, 2 ], [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2 ], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 ], [ 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 2 ], [ 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 2 ], [ 0, 2, 1, 2, 0, 0, 1, 0, 2, 2, 2, 2 ], [ 0, 2, 0, 1, 0, 0, 1, 0, 2, 0, 0, 2 ], [ 0, 2, 1, 1, 0, 0, 1, 0, 2, 0, 0, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ] ], "rule_severities": { "bus_definition_conflict": "error", "bus_entry_needed": "error", "bus_to_bus_conflict": "error", "bus_to_net_conflict": "error", "conflicting_netclasses": "error", "different_unit_footprint": "error", "different_unit_net": "error", "duplicate_reference": "error", "duplicate_sheet_names": "error", "endpoint_off_grid": "warning", "extra_units": "error", "global_label_dangling": "warning", "hier_label_mismatch": "error", "label_dangling": "error", "lib_symbol_issues": "warning", "missing_bidi_pin": "warning", "missing_input_pin": "warning", "missing_power_pin": "error", "missing_unit": "warning", "multiple_net_names": "warning", "net_not_bus_member": "warning", "no_connect_connected": "warning", "no_connect_dangling": "warning", "pin_not_connected": "error", "pin_not_driven": "error", "pin_to_pin": "warning", "power_pin_not_driven": "error", "similar_labels": "warning", "simulation_model_issue": "error", "unannotated": "error", "unit_value_mismatch": "error", "unresolved_variable": "error", "wire_dangling": "error" } }, "libraries": { "pinned_footprint_libs": [], "pinned_symbol_libs": [] }, "meta": { "filename": "templates_project.kicad_pro", "version": 1 }, "net_settings": { "classes": [ { "bus_width": 12, "clearance": 0.2, "diff_pair_gap": 0.25, "diff_pair_via_gap": 0.25, "diff_pair_width": 0.2, "line_style": 0, "microvia_diameter": 0.3, "microvia_drill": 0.1, "name": "Default", "pcb_color": "rgba(0, 0, 0, 0.000)", "schematic_color": "rgba(0, 0, 0, 0.000)", "track_width": 0.25, "via_diameter": 0.8, "via_drill": 0.4, "wire_width": 6 } ], "meta": { "version": 3 }, "net_colors": null, "netclass_assignments": null, "netclass_patterns": [] }, "pcbnew": { "last_paths": { "gencad": "", "idf": "", "netlist": "", "specctra_dsn": "", "step": "", "vrml": "" }, "page_layout_descr_file": "" }, "schematic": { "annotate_start_num": 0, "drawing": { "dashed_lines_dash_length_ratio": 12.0, "dashed_lines_gap_length_ratio": 3.0, "default_line_thickness": 6.0, "default_text_size": 50.0, "field_names": [], "intersheets_ref_own_page": false, "intersheets_ref_prefix": "", "intersheets_ref_short": false, "intersheets_ref_show": false, "intersheets_ref_suffix": "", "junction_size_choice": 3, "label_size_ratio": 0.25, "pin_symbol_size": 25.0, "text_offset_ratio": 0.08 }, "legacy_lib_dir": "", "legacy_lib_list": [], "meta": { "version": 1 }, "net_format_name": "", "page_layout_descr_file": "", "plot_directory": "", "spice_current_sheet_as_root": false, "spice_external_command": "spice \"%I\"", "spice_model_current_sheet_as_root": true, "spice_save_all_currents": false, "spice_save_all_voltages": false, "subpart_first_id": 65, "subpart_id_separator": 0 }, "sheets": [ [ "b588025f-8c23-406b-a049-ad8593913ab0", "" ] ], "text_variables": {} } ================================================ FILE: kintree/kicad/templates_project/templates_project.kicad_sch ================================================ (kicad_sch (version 20230121) (generator eeschema) (uuid b588025f-8c23-406b-a049-ad8593913ab0) (paper "A4") (lib_symbols ) (sheet_instances (path "/" (page "1")) ) ) ================================================ FILE: kintree/kintree_gui.py ================================================ import flet as ft from .gui.gui import kintree_gui def main(view='flet_app'): if view == 'browser': ft.app(target=kintree_gui, view=ft.AppView.WEB_BROWSER) return ft.app(target=kintree_gui, view=ft.AppView.FLET_APP) ================================================ FILE: kintree/search/automationdirect_api.py ================================================ from ..common.tools import download # These are the 'keys' we want to pull out response SEARCH_HEADERS = [ 'item_code', # name 'primary_desc', # description 'revision', # revision 'keywords', # keywords 'item_code', # suppli er_part_number 'manufacturer_name', # manufacturer_name 'item_code', # manufacturer_part_number 'url_fullpath', # supplier_link 'spec_url', # datasheet 'image_file_name', # image 'insert_url', # insert PD 'orderable_flg', 'prod_status', 'price', 'manual_url', # not full path to html page, value is filename.html 'unit_of_measure', 'leadtime_cd', 'production_time', 'warranty', ] PARAMETERS_MAP = [ 'tech_attributes', # List of parameters, not list of dictionaries, changes based on product returned ] PRICING_MAP = [ 'ordering_attributes', # List, e.g. ['Is Cut To Length: True', 'Maximum Cut Length: 2500', 'Minimum Cut Length: 25'] 'price', # Automation Direct only has one price, no price breaks 'unit_of_measure', # e.g. 'FT' ] def get_default_search_keys(): return [ # Order matters 'item_code', # name 'primary_desc', # description 'revision', # revision 'keywords', # keywords 'item_code', # supplier_part_number 'manufacturer_name', # manufacturer_name 'item_code', # manufacturer_part_number 'url_fullpath', # supplier_link 'spec_url', # datasheet 'image_file_name', # image ] def find_categories(part_details: str): ''' Find categories ''' try: return part_details['parentCatalogName'], part_details['catalogName'] except: return None, None def fetch_part_info(part_number: str, silent=False) -> dict: ''' Fetch part data from API ''' # Load Automation Direct settingss import re from ..common.tools import cprint from ..config import settings, config_interface automationdirect_api_settings = config_interface.load_file(settings.CONFIG_AUTOMATIONDIRECT_API) part_info = {} def search_timeout(timeout=10): 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 response = download(url, timeout=timeout) return response # Query part number try: part = search_timeout() part = part['solrResult']['response'] # extract the data for parts returned if part['numFound'] > 0: if part['numFound'] == 1: cprint(f'[INFO]\tFound exactly one result for "{part_number}"', silent=True) else: cprint(f'[INFO]\tFound {part["numFound"]} results for "{part_number}", selecting first result', silent=False) part = part['docs'][0] # choose the first part in the returned returned list else: part = None except Exception as e: cprint(f'[INFO]\tError: fetch_part_info(): {repr(e)}') part = None if not part: return part_info category, subcategory = find_categories(part) try: part_info['category'] = category part_info['subcategory'] = subcategory except: part_info['category'] = '' part_info['subcategory'] = '' headers = SEARCH_HEADERS # keys we want to search for # Get all returned data we want for key in part: if key in headers: if key == 'image_file_name': # JSON only returns image name, need to add path try: part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_IMAGE_PATH', '') + part['image_file_name'] except IndexError: pass elif key == 'spec_url': # datasheet url returns partial path, need to add ROOT URL try: part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + part['spec_url'] except IndexError: pass elif key == 'insert_url': # insert url returns partial path, need to add ROOT URL try: part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + part['insert_url'] except IndexError: pass elif key == 'manual_url': # manul url returns .html file name, need to build the rest of the URL try: part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + '/static/manuals/' + str(part['manual_url']).rsplit('.', 1)[0] + '/' + part['manual_url'] except IndexError: pass elif key == 'url_fullpath': # despite being named fullpath, the URL needs the TLD as a prefix try: part_info[key] = automationdirect_api_settings.get('AUTOMATIONDIRECT_API_ROOT_URL', '') + '/adc/shopping' + str(part['url_fullpath']) except IndexError: pass elif key == 'manufacturer_name': # taken care of in parameter list below pass else: part_info[key] = part[key] # Parameters part_info['parameters'] = {} [parameter_key] = PARAMETERS_MAP if part.get(parameter_key, ''): for attribute in part[parameter_key]: attribute_list = [x.strip() for x in attribute.split(':')] parameter_name = attribute_list[0] parameter_name = parameter_name.replace('/', '') parameter_value = attribute_list[1] try: html_li_list = re.split(r"]*\s*>|(\&(?:[\w\d]+|#\d+|#x[a-f\d]+);)", parameter_value) cleaned_html_li_list = list(filter(None, html_li_list)) parameter_value = ', '.join(cleaned_html_li_list) except Exception as e: print(f'{repr(e)}') if parameter_name == "Brand": # Manufacturer Name returned as a parameter, pick it out of parameters list aand store it appropriately part_info['manufacturer_name'] = parameter_value # Nominal Input Voltage gives range min-max, parse it out to put in min/max params if parameter_name == "Nominal Input Voltage": if parameter_value.count('-') == 1: parameter_value = re.sub(r'[^\d-]+', '', parameter_value) values_list = parameter_value.split('-') min_value = min(values_list) max_value = max(values_list) part_info['parameters']['Min Input Voltage'] = min_value part_info['parameters']['Max Input Voltage'] = max_value else: # more than one range, copy into set param fields part_info['parameters']['Min Input Voltage'] = parameter_value part_info['parameters']['Max Input Voltage'] = parameter_value # Nominal Output Voltage gives range min-max, parse it out to put in min/max params if parameter_name == "Nominal Output Voltage": if parameter_value.count('-') == 1: parameter_value = re.sub(r'[^\d-]+', '', parameter_value) values_list = parameter_value.split('-') min_value = min(values_list) max_value = max(values_list) part_info['parameters']['Min Output Voltage'] = min_value part_info['parameters']['Max Output Voltage'] = max_value else: # more than one range, copy into set param fields part_info['parameters']['Min Output Voltage'] = parameter_value part_info['parameters']['Max Output Voltage'] = parameter_value else: # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value # Pricing part_info['pricing'] = {} [ordering_attributes, price_key, unit_per_price] = PRICING_MAP # Parse out ordering attributes pricing_attributes = {} price_per_unit = part.get(price_key, '0') try: for attribute in part[ordering_attributes]: attribute = attribute.split(':') attribute = [x.strip() for x in attribute] pricing_attributes[str(attribute[0])] = attribute[1] min_quantity = int(pricing_attributes['Minimum Cut Length']) max_quanitity = int(pricing_attributes['Maximum Cut Length']) price_per_unit = part[price_key] # Automation Direct doesn't have price breaks, but we can create common set quanitities for reference quantities = [100, 250, 500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 12000, 14000, 15000] quantities.insert(0, min_quantity) quantities.append(max_quanitity) quantities.sort() quantities = [qty for qty in quantities if qty <= max_quanitity] for i in range(len(quantities) - 1): part_info['pricing'][quantities[i]] = price_per_unit except KeyError as e: from ..common.tools import cprint cprint(f'[INFO]\tNo pricing attribute "{e.args[0]}" found for "{part_number}"', silent=silent) part_info['pricing']['1'] = price_per_unit part_info['currency'] = 'USD' # Extra search fields if settings.CONFIG_AUTOMATIONDIRECT.get('EXTRA_FIELDS', None): for extra_field in settings.CONFIG_AUTOMATIONDIRECT['EXTRA_FIELDS']: if part.get(extra_field, None): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) return part_info def test_api() -> bool: ''' Test method for API ''' test_success = True expected = { 'image_file_name': 'https://cdn.automationdirect.com/images/products/medium/m_bx16nd3.jpg', 'item_code': 'BX-16ND3', 'manual_url': 'https://www.automationdirect.com/static/manuals/brxuserm/brxuserm.html', 'unit_of_measure': 'EA', "parameters": { 'Brand': 'BRX', 'Item': 'Input module', 'IO Module Type': 'Discrete', 'Number of Input Points': '16', 'Min Input Voltage': '12', 'Max Input Voltage': '24', 'Nominal Input Voltage': '12-24', 'Discrete Input Type': 'Sinking/sourcing', 'Fast Response': 'No', 'Number of Isolated Input Commons': '4', 'Number of Points per Common': '4', 'Requires': 'BX-RTB10, BX-RTB10-1 or BX-RTB10-2 terminal block kit or ZIPLink pre-wired cables', 'Programming Software': 'Do-more Designer programming software v2.0 or later' } } test_part = fetch_part_info('BX-16ND3', silent=True) if not test_part: test_success = False # Check content of response if test_success: for key, value in expected.items(): if test_part[key] != value: print(f'"{test_part[key]}" <> "{value}"') test_success = False break return test_success ================================================ FILE: kintree/search/digikey_api.py ================================================ import logging import os import digikey from ..config import settings, config_interface SEARCH_HEADERS = [ 'description', 'digi_key_part_number', 'manufacturer', 'manufacturer_product_number', 'product_url', 'datasheet_url', 'photo_url', ] PARAMETERS_MAP = [ 'parameters', 'parameter_text', 'value_text', ] PRICING_MAP = [ 'product_variations', 'digi_key_product_number', 'standard_pricing', 'break_quantity', 'unit_price', 'package_type' ] os.environ['DIGIKEY_STORAGE_PATH'] = settings.DIGIKEY_STORAGE_PATH # Check if storage path exists, else create it if not os.path.exists(os.environ['DIGIKEY_STORAGE_PATH']): os.makedirs(os.environ['DIGIKEY_STORAGE_PATH'], exist_ok=True) def disable_api_logger(): # Digi-Key API logger logging.getLogger('digikey.v3.api').setLevel(logging.CRITICAL) # Disable DEBUG logging.disable(logging.DEBUG) def check_environment() -> bool: DIGIKEY_CLIENT_ID = os.environ.get('DIGIKEY_CLIENT_ID', None) DIGIKEY_CLIENT_SECRET = os.environ.get('DIGIKEY_CLIENT_SECRET', None) if not DIGIKEY_CLIENT_ID or not DIGIKEY_CLIENT_SECRET: return False return True def setup_environment(force=False) -> bool: if not check_environment() or force: # SETUP the Digikey authentication see https://developer.digikey.com/documentation/organization#production digikey_api_settings = config_interface.load_file(settings.CONFIG_DIGIKEY_API) os.environ['DIGIKEY_CLIENT_ID'] = digikey_api_settings['DIGIKEY_CLIENT_ID'] os.environ['DIGIKEY_CLIENT_SECRET'] = digikey_api_settings['DIGIKEY_CLIENT_SECRET'] os.environ['DIGIKEY_LOCAL_SITE'] = digikey_api_settings.get('DIGIKEY_LOCAL_SITE', 'US') os.environ['DIGIKEY_LOCAL_LANGUAGE'] = digikey_api_settings.get('DIGIKEY_LOCAL_LANGUAGE', 'en') os.environ['DIGIKEY_LOCAL_CURRENCY'] = digikey_api_settings.get('DIGIKEY_LOCAL_CURRENCY', 'USD') return check_environment() def get_default_search_keys(): return [ 'product_description', 'product_description', 'revision', 'keywords', 'digi_key_part_number', 'manufacturer', 'manufacturer_product_number', 'product_url', 'datasheet_url', 'photo_url', ] def find_categories(part_details: str): ''' Find categories ''' category = part_details.get('category') subcategory = None if category: subcategory = category.get('child_categories')[0] category = category.get('name') if subcategory: subcategory = subcategory.get('name') return category, subcategory def fetch_part_info(part_number: str) -> dict: ''' Fetch part data from API ''' from wrapt_timeout_decorator import timeout part_info = {} if not setup_environment(): from ..common.tools import cprint cprint('[INFO]\tWarning: DigiKey API settings are not configured') return part_info # THIS METHOD CAN SOMETIMES RETURN INCORRECT MATCH # Added logic to check the result in the GUI flow @timeout(dec_timeout=20) def digikey_search_timeout(): return digikey.product_details( part_number, x_digikey_locale_site=os.environ['DIGIKEY_LOCAL_SITE'], x_digikey_locale_language=os.environ['DIGIKEY_LOCAL_LANGUAGE'], x_digikey_locale_currency=os.environ['DIGIKEY_LOCAL_CURRENCY'], ).to_dict() # Method to process price breaks def process_price_break(product_variation): part_info['digi_key_part_number'] = product_variation.get(digi_number_key) for price_break in product_variation[pricing_key]: quantity = price_break[qty_key] price = price_break[price_key] part_info['pricing'][quantity] = price # Query part number try: part = digikey_search_timeout() except: part = None if not part: return part_info if 'product' not in part or not part['product']: return part_info part_info['currency'] = part['search_locale_used']['currency'] part = part['product'] category, subcategory = find_categories(part) try: part_info['category'] = category part_info['subcategory'] = subcategory except: part_info['category'] = '' part_info['subcategory'] = '' headers = SEARCH_HEADERS for key in part: if key in headers: if key == 'manufacturer': part_info[key] = part['manufacturer'].get('name') elif key == 'description': part_info['product_description'] = part['description'].get('product_description') part_info['detailed_description'] = part['description'].get('detailed_description') else: part_info[key] = part[key] # Parameters part_info['parameters'] = {} [parameter_key, name_key, value_key] = PARAMETERS_MAP for parameter in part[parameter_key]: parameter_name = parameter.get(name_key, '') parameter_value = parameter.get(value_key, '') # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value # process classifications as parameters for classification, value in part.get('classifications', {}).items(): part_info['parameters'][classification] = value # Pricing part_info['pricing'] = {} [variations_key, digi_number_key, pricing_key, qty_key, price_key, package_key] = PRICING_MAP variations = part[variations_key] if len(variations) == 1: process_price_break(variations[0]) else: for variation in variations: # we try to get the not TR or Digi-Reel option package_type = variation.get(package_key).get('id') if all(package_type != x for x in [1, 243]): process_price_break(variation) break # if no other option was found use the first one returned if not part_info['pricing'] and variations: process_price_break(variations[0]) # Extra search fields if settings.CONFIG_DIGIKEY.get('EXTRA_FIELDS'): for extra_field in settings.CONFIG_DIGIKEY['EXTRA_FIELDS']: if part.get(extra_field): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) return part_info def test_api(check_content=False) -> bool: ''' Test method for API token ''' test_success = True expected = { 'product_description': 'RES 10K OHM 5% 1/16W 0402', 'digi_key_part_number': 'RMCF0402JT10K0CT-ND', 'manufacturer': 'Stackpole Electronics Inc', 'manufacturer_product_number': 'RMCF0402JT10K0', 'product_url': 'https://www.digikey.com/en/products/detail/stackpole-electronics-inc/RMCF0402JT10K0/1758206', 'datasheet_url': 'https://www.seielect.com/catalog/sei-rmcf_rmcp.pdf', 'photo_url': 'https://mm.digikey.com/Volume0/opasdata/d220001/medias/images/2597/MFG_RMC SERIES.jpg', } test_part = fetch_part_info('RMCF0402JT10K0') # Check for response if not test_part: test_success = False if not check_content: return test_success # Check content of response if test_success: for key, value in expected.items(): if test_part[key] != value: print(f'{test_part[key]} != {value}') test_success = False break return test_success ================================================ FILE: kintree/search/element14_api.py ================================================ from ..config import settings, config_interface from ..common.tools import download ELEMENT14_API_URL = 'https://api.element14.com/catalog/products' STORES = { 'Farnell': { 'Bulgaria': 'bg.farnell.com ', 'Czechia': 'cz.farnell.com', 'Denmark': 'dk.farnell.com', 'Austria': 'at.farnell.com ', 'Switzerland': 'ch.farnell.com', 'Germany': 'de.farnell.com', 'CPC UK': 'cpc.farnell.com', 'CPC Ireland': 'cpcireland.farnell.com', 'Export': 'export.farnell.com', 'Onecall': 'onecall.farnell.com', 'Ireland': 'ie.farnell.com', 'Israel': 'il.farnell.com', 'United Kingdom': 'uk.farnell.com', 'Spain': 'es.farnell.com', 'Estonia': 'ee.farnell.com', 'Finland': 'fi.farnell.com', 'France': 'fr.farnell.com', 'Hungary': 'hu.farnell.com', 'Italy': 'it.farnell.com', 'Lithuania': 'lt.farnell.com', 'Latvia': 'lv.farnell.com', 'Belgium': 'be.farnell.com', 'Netherlands': 'nl.farnell.com', 'Norway': 'no.farnell.com', 'Poland': 'pl.farnell.com', 'Portugal': 'pt.farnell.com', 'Romania': 'ro.farnell.com', 'Russia': 'ru.farnell.com', 'Slovakia': 'sk.farnell.com', 'Slovenia': 'si.farnell.com', 'Sweden': 'se.farnell.com', 'Turkey': 'tr.farnell.com', }, 'Newark': { 'Canada': 'canada.newark.com', 'Mexico': 'mexico.newark.com', 'United States': 'www.newark.com', }, 'Element14': { 'China': 'cn.element14.com', 'Australia': 'au.element14.com', 'New Zealand': 'nz.element14.com', 'Hong Kong': 'hk.element14.com', 'Singapore': 'sg.element14.com', 'Malaysia': 'my.element14.com', 'Philippines': 'ph.element14.com', 'Thailand': 'th.element14.com', 'India': 'in.element14.com', 'Taiwan': 'tw.element14.com', 'Korea': 'kr.element14.com', 'Vietnam': 'vn.element14.com', }, } SEARCH_HEADERS = [ 'brandName', 'displayName', 'sku', 'translatedManufacturerPartNumber', 'datasheets', 'image', 'attributes', ] PARAMETERS_MAP = [ 'attributes', 'attributeLabel', 'attributeValue', ] PRICING_MAP = [ 'prices', 'from', 'cost', ] CURRENCIES = { STORES['Farnell']['Bulgaria']: 'EUR', STORES['Farnell']['Czechia']: 'CZK', STORES['Farnell']['Denmark']: 'DKK', STORES['Farnell']['Austria']: 'EUR', STORES['Farnell']['Switzerland']: 'CHF', STORES['Farnell']['Germany']: 'EUR', STORES['Farnell']['CPC UK']: 'GBP', STORES['Farnell']['CPC Ireland']: 'EUR', STORES['Farnell']['Export']: 'GBP', STORES['Farnell']['Onecall']: 'GBP', STORES['Farnell']['Ireland']: 'EUR', STORES['Farnell']['Israel']: 'USD', STORES['Farnell']['United Kingdom']: 'GBP', STORES['Farnell']['Spain']: 'EUR', STORES['Farnell']['Estonia']: 'EUR', STORES['Farnell']['Finland']: 'EUR', STORES['Farnell']['France']: 'EUR', STORES['Farnell']['Hungary']: 'HUF', STORES['Farnell']['Italy']: 'EUR', STORES['Farnell']['Lithuania']: 'EUR', STORES['Farnell']['Latvia']: 'EUR', STORES['Farnell']['Belgium']: 'EUR', STORES['Farnell']['Netherlands']: 'EUR', STORES['Farnell']['Norway']: 'NOK', STORES['Farnell']['Poland']: 'PLN', STORES['Farnell']['Portugal']: 'EUR', STORES['Farnell']['Romania']: 'RON', STORES['Farnell']['Russia']: 'EUR', STORES['Farnell']['Slovakia']: 'EUR', STORES['Farnell']['Slovenia']: 'EUR', STORES['Farnell']['Sweden']: 'SEK', STORES['Farnell']['Turkey']: 'EUR', STORES['Newark']['Canada']: 'CAD', STORES['Newark']['Mexico']: 'USD', STORES['Newark']['United States']: 'USD', STORES['Element14']['China']: 'CNY', STORES['Element14']['Australia']: 'AUD', STORES['Element14']['New Zealand']: 'NZD', STORES['Element14']['Hong Kong']: 'HKD', STORES['Element14']['Singapore']: 'SGD', STORES['Element14']['Malaysia']: 'MYR', STORES['Element14']['Philippines']: 'PHP', STORES['Element14']['Thailand']: 'THB', STORES['Element14']['India']: 'INR', STORES['Element14']['Taiwan']: 'TWD', STORES['Element14']['Korea']: 'KRW', STORES['Element14']['Vietnam']: 'USD', } def get_default_search_keys(): return [ 'displayName', 'displayName', 'revision', 'keywords', 'sku', 'brandName', 'translatedManufacturerPartNumber', 'store_url', 'datasheet_url', 'image_url', ] def get_default_store_url(supplier: str) -> str: ''' Get saved store/location for supplier ''' import re user_settings = config_interface.load_file(settings.CONFIG_ELEMENT14_API) default_store = user_settings.get(f'{supplier.upper()}_STORE', '') if not default_store: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Default store "{supplier.upper()}_STORE" value not configured', silent=False) url_match = re.match(r'^(.+?) \((.+?)\)$', default_store) if url_match: return url_match.group(2) return STORES[supplier][default_store] def build_api_url(part_number: str, supplier: str, store_url=None, silent=False) -> str: ''' Build API URL based on user settings ''' user_settings = config_interface.load_file(settings.CONFIG_ELEMENT14_API) api_key = user_settings.get('ELEMENT14_PRODUCT_SEARCH_API_KEY', '') if not api_key: from ..common.tools import cprint cprint('[INFO]\tWarning: ELEMENT14_PRODUCT_SEARCH_API_KEY user value not configured', silent=silent) import os api_key = os.environ.get('ELEMENT14_PART_API_KEY', None) if not api_key: cprint('[INFO]\tWarning: ELEMENT14_PRODUCT_SEARCH_API_KEY env variable value not found', silent=False) if not store_url: store_url = get_default_store_url(supplier) # Set base URL api_url = ELEMENT14_API_URL # Set response format api_url += '?callInfo.responseDataFormat=JSON' # Set result settings: offset = 0; number of results = 1; size = large (eg. to get attributes) api_url += '&resultsSettings.offset=0&resultsSettings.numberOfResults=1&resultsSettings.responseGroup=large' # Set API key api_url += f'&callInfo.apiKey={api_key}' # Set store URL api_url += f'&storeInfo.id={store_url}' # Set part number api_url += f'&term=manuPartNum:{part_number}' return api_url def build_image_url(image_data: dict, supplier: str, store_url=None) -> str: image_url = 'https://' # Set store URL if store_url: image_url += store_url else: image_url += get_default_store_url(supplier) # Append static text image_url += '/productimages/standard' # Append locale if 'farnell' in image_data['vrntPath']: image_url += '/en_GB' else: image_url += '/en_US' # Append image filename image_url += image_data['baseName'] return image_url def fetch_part_info(part_number: str, supplier: str, store_url=None, silent=False) -> dict: ''' Fetch part data from API ''' part_info = {} def search_timeout(timeout=10): url = build_api_url(part_number, supplier, store_url, silent) response = download(url, timeout=timeout) return response # Query part number try: part = search_timeout() except: part = None # Extract result try: part = part['manufacturerPartNumberSearchReturn'].get('products', [])[0] except (TypeError, IndexError): part = None if not part: return part_info headers = SEARCH_HEADERS for key in part: if key in headers: if key == 'displayName': # String to remove str_remove = part['brandName'] + ' - ' + part['translatedManufacturerPartNumber'] + ' - ' # Remove and limit to 100 chars part_info['displayName'] = part['displayName'].replace(str_remove, '')[:100] elif key == 'datasheets': try: part_info['datasheet_url'] = part['datasheets'][0]['url'].replace('http', 'https') except IndexError: pass elif key == 'image': part_info['image_url'] = build_image_url(part['image'], supplier, store_url) elif key == 'attributes': part_info['parameters'] = {} else: part_info[key] = part[key] # Parameters if 'parameters' in part_info.keys(): [parameter_key, name_key, value_key] = PARAMETERS_MAP try: for parameter in range(len(part[parameter_key])): parameter_name = part[parameter_key][parameter][name_key] parameter_value = part[parameter_key][parameter][value_key] # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value except TypeError: # Parameter list is empty pass # Pricing part_info['pricing'] = {} [pricing_key, qty_key, price_key] = PRICING_MAP for price_break in part[pricing_key]: quantity = price_break[qty_key] price = price_break[price_key] part_info['pricing'][quantity] = price if not store_url: store_url = get_default_store_url(supplier) part_info['currency'] = CURRENCIES.get(store_url, 'USD') # Extra search fields if settings.CONFIG_ELEMENT14.get('EXTRA_FIELDS', None): for extra_field in settings.CONFIG_ELEMENT14['EXTRA_FIELDS']: if part.get(extra_field, None): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) # Append Store URL # Element14 support said "At this time our API is not structured to provide a URL to product pages in the selected storeInfo.id value." if store_url: part_info['store_url'] = f'https://{store_url}' else: part_info['store_url'] = f'https://{get_default_store_url(supplier)}' # Append search to URL part_info['store_url'] += f'/w/search?st={part["translatedManufacturerPartNumber"]}' # Append categories part_info['category'] = '' part_info['subcategory'] = '' return part_info def test_api(store_url=None) -> bool: ''' Test method for API ''' test_success = True search_queries = [ { 'store_url': 'uk.farnell.com', 'part_number': '1N4148W-7-F', 'expected': { 'displayName': 'DIODE, ULTRAFAST RECOVERY, 300mA, 75V, SOD-123-2, FULL REEL', 'brandName': 'MULTICOMP PRO', 'translatedManufacturerPartNumber': '1N4148W-7-F.', } }, { 'store_url': 'www.newark.com', 'part_number': 'BLM18AG601SN1D', 'expected': { 'displayName': 'Ferrite Bead, 0603 [1608 Metric], 600 ohm, 500 mA, EMIFIL BLM18AG Series, 0.38 ohm, ± 25%', 'brandName': 'MURATA', 'translatedManufacturerPartNumber': 'BLM18AG601SN1D', } }, { 'store_url': 'au.element14.com', 'part_number': '2N7002K-T1-GE3', 'expected': { 'displayName': 'Power MOSFET, N Channel, 60 V, 190 mA, 2 ohm, SOT-23, Surface Mount', 'brandName': 'VISHAY', 'translatedManufacturerPartNumber': '2N7002K-T1-GE3', } }, ] if store_url: # If store URL is specified, only check data is returned (eg. avoid discrepancies between stores) part_number = '1N4148' test_part = fetch_part_info(part_number, '', store_url, True) if not test_part: test_success = False else: for item in search_queries: if not test_success: break test_part = fetch_part_info(item['part_number'], '', item['store_url'], True) if not test_part: test_success = False # Check content of response if test_success: for key, value in item['expected'].items(): if test_part[key] != value: print(f'"{test_part[key]}" <> "{value}"') test_success = False break return test_success ================================================ FILE: kintree/search/jameco_api.py ================================================ import html import re from ..common.tools import download SEARCH_HEADERS = [ 'title', 'name', 'prod_id', 'ss_attr_manufacturer', 'manufacturer_part_number', 'url', 'imageUrl', 'related_prod_id', 'category', ] # Not really a map for Jameco. # Parameters are listed at same level as the search keys, not in separate list PARAMETERS_KEYS = [ 'product_type_unigram', 'ss_attr_voltage_rating', 'ss_attr_multiple_order_quantity', ] def get_default_search_keys(): # order matters, linked with part_form[] order in inventree_interface.translate_supplier_to_form() return [ 'title', 'name', 'revision', 'keywords', 'prod_id', 'ss_attr_manufacturer', 'manufacturer_part_number', 'url', 'datasheet', 'imageUrl', ] def find_categories(part_details: str): ''' Find categories ''' try: return part_details['parentCatalogName'], part_details['catalogName'] except: return None, None def fetch_part_info(part_number: str) -> dict: ''' Fetch part data from API ''' # Load Jameco settings from ..config import settings, config_interface jameco_api_settings = config_interface.load_file(settings.CONFIG_JAMECO_API) part_info = {} def search_timeout(timeout=10): url = jameco_api_settings.get('JAMECO_API_URL', '') + part_number response = download(url, timeout=timeout) return response # Query part number try: part = search_timeout() # Extract results, select first in returned search List part = part.get('results', None) part = part[0] except: part = None if not part: return part_info category, subcategory = find_categories(part) try: part_info['category'] = category part_info['subcategory'] = subcategory except: part_info['category'] = '' part_info['subcategory'] = '' headers = SEARCH_HEADERS for key in part: if key in headers: if key == 'imageUrl': try: part_info[key] = part['imageUrl'] except IndexError: pass elif key in ['title', 'name', 'category']: # Jameco title/name is often >100 chars, which causes an error later. Check for it here. if (len(part[key]) > 100): trimmed_value = str(part[key])[:100] part_info[key] = html.unescape(trimmed_value) # Json data sometimes has HTML encoded chars, e.g. " else: part_info[key] = html.unescape(part[key]) else: part_info[key] = part[key] # Parameters part_info['parameters'] = {} for i, parameter_key in enumerate(PARAMETERS_KEYS): if part.get(parameter_key, ''): parameter_name = parameter_key parameter_value = part[parameter_key] if isinstance(parameter_value, list): parameter_string = ', '.join(parameter_value) part_info['parameters'][parameter_name] = parameter_string else: # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value # Pricing part_info['pricing'] = {} # Jameco returns price breaks as a string of HTML text # Convert pricing string pattern to List, then dictionary for Ki-nTree price_break_str = part['secondary_prices'] price_break_str = price_break_str.replace(',', '') # remove comma price_break_str = re.sub(r'(\<br\s\/&*gt)', '', price_break_str) # remove HTML price_break_str = re.sub(';', ':', price_break_str) # remove ; char price_break_str = re.sub(r'(\:\s+\$)|\;', ':', price_break_str) # remove $ char price_break_list = price_break_str.split(':') # split on : price_break_list.pop() # remove last empty element in List for i in range(0, len(price_break_list), 2): quantity = int(price_break_list[i]) price = float(price_break_list[i + 1]) part_info['pricing'][quantity] = price part_info['currency'] = 'USD' # Extra search fields if settings.CONFIG_JAMECO.get('EXTRA_FIELDS', None): for extra_field in settings.CONFIG_JAMECO['EXTRA_FIELDS']: if part.get(extra_field, None): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) return part_info def test_api() -> bool: ''' Test method for API ''' test_success = True expected = { 'manufacturer_part_number': 'PN2222ABU', 'name': 'Transistor PN2222A NPN Silicon General Purpose TO-92', 'prod_id': '178511', } test_part = fetch_part_info('178511') if not test_part: test_success = False # Check content of response if test_success: for key, value in expected.items(): if test_part[key] != value: print(f'"{test_part[key]}" <> "{value}"') test_success = False break return test_success ================================================ FILE: kintree/search/lcsc_api.py ================================================ from ..common.tools import download SEARCH_HEADERS = [ 'productDescEn', 'productIntroEn', 'productCode', 'brandNameEn', 'productModel', 'pdfUrl', 'productImages', ] PARAMETERS_MAP = [ 'paramVOList', 'paramNameEn', 'paramValueEn', ] PRICING_MAP = [ 'productPriceList', 'ladder', 'usdPrice', ] def get_default_search_keys(): return [ 'productIntroEn', 'productIntroEn', 'revision', 'keywords', 'productCode', 'brandNameEn', 'productModel', 'part_url', 'pdfUrl', 'productImages', ] def find_categories(part_details: str): ''' Find categories ''' try: return part_details['parentCatalogName'], part_details['catalogName'] except: return None, None def fetch_part_info(part_number: str) -> dict: ''' Fetch part data from API ''' # Load LCSC settings from ..config import settings, config_interface lcsc_api_settings = config_interface.load_file(settings.CONFIG_LCSC_API) part_info = {} def search_timeout(timeout=10): url = lcsc_api_settings.get('LCSC_API_URL', '') + part_number response = download(url, timeout=timeout) return response # Query part number try: part = search_timeout() # Extract result part = part.get('result', None) except: part = {} if not part: return part_info product_code = part.get('productCode') if product_code: part_info['part_url'] = f'https://www.lcsc.com/product-detail/{product_code}.html' category, subcategory = find_categories(part) try: part_info['category'] = category part_info['subcategory'] = subcategory except: part_info['category'] = '' part_info['subcategory'] = '' headers = SEARCH_HEADERS for key in part: if key in headers: if key == 'productImages': try: part_info[key] = part['productImages'][0] except IndexError: pass else: part_info[key] = part[key] # Parameters part_info['parameters'] = {} [parameter_key, name_key, value_key] = PARAMETERS_MAP if part.get(parameter_key, ''): for parameter in range(len(part[parameter_key])): parameter_name = part[parameter_key][parameter][name_key] parameter_value = part[parameter_key][parameter][value_key] # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value # Pricing part_info['pricing'] = {} [pricing_key, qty_key, price_key] = PRICING_MAP for price_break in part[pricing_key]: quantity = price_break[qty_key] price = price_break[price_key] part_info['pricing'][quantity] = price part_info['currency'] = 'USD' # Extra search fields if settings.CONFIG_LCSC.get('EXTRA_FIELDS', None): for extra_field in settings.CONFIG_LCSC['EXTRA_FIELDS']: if part.get(extra_field, None): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) return part_info def test_api() -> bool: ''' Test method for API ''' test_success = True expected = { 'productIntroEn': '25V 100pF C0G ±5% 0201 Multilayer Ceramic Capacitors MLCC - SMD/SMT ROHS', 'productCode': 'C2181718', 'brandNameEn': 'TDK', 'productModel': 'C0603C0G1E101J030BA', } test_part = fetch_part_info('C2181718') if not test_part: test_success = False # Check content of response if test_success: for key, value in expected.items(): if test_part[key] != value: print(f'"{test_part[key]}" <> "{value}"') test_success = False break return test_success ================================================ FILE: kintree/search/mouser_api.py ================================================ import os from ..config import settings, config_interface from mouser.api import MouserPartSearchRequest SEARCH_HEADERS = [ 'Description', 'productCode', 'MouserPartNumber', 'Manufacturer', 'ManufacturerPartNumber', 'DataSheetUrl', 'ProductDetailUrl', 'ImagePath', ] PARAMETERS_MAP = [ 'ProductAttributes', 'AttributeName', 'AttributeValue', ] PRICING_MAP = [ 'PriceBreaks', 'Quantity', 'Price', 'Currency', ] def get_default_search_keys(): return [ 'ManufacturerPartNumber', 'Description', 'revision', 'keywords', 'MouserPartNumber', 'Manufacturer', 'ManufacturerPartNumber', 'ProductDetailUrl', 'DataSheetUrl', 'ImagePath', ] def setup_environment(force=False): ''' Setup environmental variables ''' api_key = os.environ.get('MOUSER_PART_API_KEY', None) if not api_key or force: mouser_api_settings = config_interface.load_file(settings.CONFIG_MOUSER_API) try: os.environ['MOUSER_PART_API_KEY'] = mouser_api_settings['MOUSER_PART_API_KEY'] except TypeError: pass def find_categories(part_details: str): ''' Find categories ''' try: return part_details['Category'], None except: return None, None def fetch_part_info(part_number: str) -> dict: ''' Fetch part data from API ''' from wrapt_timeout_decorator import timeout setup_environment() part_info = {} @timeout(dec_timeout=20) def search_timeout(): try: request = MouserPartSearchRequest('partnumber') request.part_search(part_number) except FileNotFoundError as e: error_message = repr(e.args[0]) error_message = error_message.strip("'") from ..common.tools import cprint cprint(f'[INFO] Warning: {error_message}', silent=False) finally: # Mouser 0.1.6 API update: single part list is returned, instead of dict return request.get_clean_response()[0] # Query part number try: part: dict = search_timeout() except: part = None if not part: return part_info # Check for empty response empty = True for key, value in part.items(): if value: empty = False break if empty: return part_info category, subcategory = find_categories(part) try: part_info['category'] = category part_info['subcategory'] = subcategory except: part_info['category'] = '' part_info['subcategory'] = '' headers = SEARCH_HEADERS for key in part: if key in headers: part_info[key] = part[key] # Parameters part_info['parameters'] = {} [parameter_key, name_key, value_key] = PARAMETERS_MAP for parameter in range(len(part[parameter_key])): parameter_name = part[parameter_key][parameter][name_key] parameter_value = part[parameter_key][parameter][value_key] # Append to parameters dictionary part_info['parameters'][parameter_name] = parameter_value # Pricing part_info['pricing'] = {} [pricing_key, qty_key, price_key, currency_key] = PRICING_MAP for price_break in part[pricing_key]: quantity = price_break[qty_key] price = price_break[price_key] part_info['pricing'][quantity] = price if part[pricing_key]: part_info['currency'] = part[pricing_key][0][currency_key] else: part_info['currency'] = 'USD' # Extra search fields if settings.CONFIG_MOUSER.get('EXTRA_FIELDS', None): for extra_field in settings.CONFIG_MOUSER['EXTRA_FIELDS']: if part.get(extra_field, None): part_info['parameters'][extra_field] = part[extra_field] else: from ..common.tools import cprint cprint(f'[INFO]\tWarning: Extra field "{extra_field}" not found in search results', silent=False) return part_info def test_api() -> bool: ''' Test method for API ''' test_success = True expected = { 'Description': 'MOSFETs P-channel 1.25W', 'MouserPartNumber': '621-DMP2066LSN-7', 'Manufacturer': 'Diodes Incorporated', 'ManufacturerPartNumber': 'DMP2066LSN-7', } test_part = fetch_part_info('DMP2066LSN-7') if not test_part: # Unsucessful search test_success = False else: # Check content of response for key, value in expected.items(): if test_part[key] != value: print(f'"{test_part[key]}" <> "{value}"') test_success = False break return test_success ================================================ FILE: kintree/search/search_api.py ================================================ import os import time from ..config import settings, config_interface def load_from_file(search_file, test_mode=False) -> dict: ''' Fetch part data from file ''' cache_valid = settings.CACHE_VALID_DAYS * 24 * 3600 # Load data from file if cache enabled if settings.CACHE_ENABLED: try: part_data = config_interface.load_file(search_file) except FileNotFoundError: return None # Check cache validity try: # Get timestamp timestamp = int(time.time() - part_data['search_timestamp']) except (KeyError, TypeError): timestamp = int(time.time()) if timestamp < cache_valid or test_mode: return part_data return None def save_to_file(part_info, search_file, update_ts=True): ''' Save part data to file ''' # Check if search/results directory needs to be created if not os.path.exists(os.path.dirname(search_file)): os.mkdir(os.path.dirname(search_file)) if update_ts: # Update timestamp part_info['search_timestamp'] = int(time.time()) # Save data if cache enabled if settings.CACHE_ENABLED: config_interface.dump_file(part_info, search_file) ================================================ FILE: kintree/search/snapeda_api.py ================================================ from ..config import settings from ..common.tools import download, download_with_retry API_BASE_URL = 'https://snapeda.eeinte.ch/?' SNAPEDA_URL = 'https://www.snapeda.com' def fetch_snapeda_part_info(part_number: str) -> dict: ''' Fetch SnapEDA part data from API ''' api_url = API_BASE_URL + part_number.replace(' ', '%20') data = download(api_url, timeout=10) return data if data else {} def parse_snapeda_response(response: dict) -> dict: ''' Return only relevant information from SnapEDA API response ''' data = { 'part_number': None, 'has_symbol': False, 'has_footprint': False, 'symbol_image': None, 'footprint_image': None, 'package': None, 'part_url': None, 'has_single_result': False, } number_results = int(response.get('hits', 0)) # Check for single result if number_results == 1: try: data['part_number'] = response['results'][0].get('part_number', None) data['has_symbol'] = response['results'][0].get('has_symbol', False) data['has_footprint'] = response['results'][0].get('has_footprint', False) data['package'] = response['results'][0]['package'].get('name', None) data['part_url'] = SNAPEDA_URL + response['results'][0]['_links']['self'].get('href', '') data['part_url'] += '?ref=kintree' data['has_single_result'] = True except KeyError: pass # Separate as the 'models' key does not always exist try: data['symbol_image'] = response['results'][0]['models'][0]['symbol_medium'].get('url', None) except KeyError: pass try: data['footprint_image'] = response['results'][0]['models'][0]['package_medium'].get('url', None) except KeyError: pass elif number_results > 1: try: data['part_url'] = SNAPEDA_URL + '/search/' + response['pages'][0].get('link', None).split('&')[0] + '&ref=kintree' except: pass else: pass return data def download_snapeda_images(snapeda_data: dict, silent=False) -> dict: ''' Download symbol and footprint images from SnapEDA's server ''' images = { 'symbol': None, 'footprint': None, } try: part_number = snapeda_data["part_number"].replace('/', '').lower() except: part_number = None if part_number: try: if snapeda_data['symbol_image']: # Form path image_name = f'{part_number}_symbol.png' image_location = settings.search_images + image_name # Download symbol image symbol = download_with_retry( url=snapeda_data['symbol_image'], full_path=image_location, filetype='Image', silent=silent, ) if symbol: images['symbol'] = image_location except KeyError: pass try: if snapeda_data['footprint_image']: # Form path image_name = f'{part_number}_footprint.png' image_location = settings.search_images + image_name # Download symbol image footprint = download_with_retry( url=snapeda_data['footprint_image'], full_path=image_location, filetype='Image', silent=silent, ) if footprint: images['footprint'] = image_location except KeyError: pass return images def test_snapeda_api() -> bool: ''' Test method for SnapEDA API ''' result = False # Test single result response = fetch_snapeda_part_info('TPS61221DCKR') data = parse_snapeda_response(response) images = download_snapeda_images(data, silent=True) if data['part_number'] and data['has_symbol'] and images['symbol']: result = True # Test multiple results if result: response = fetch_snapeda_part_info('1N4148W-7-F') data = parse_snapeda_response(response) if data['has_single_result']: result = False return result ================================================ FILE: kintree/search/tme_api.py ================================================ import base64 import collections import hashlib import hmac import os import urllib.parse import urllib.request import json # from ..common.tools import download from ..config import config_interface, settings PRICING_MAP = [ 'PriceList', 'Amount', 'PriceValue', 'Currency', ] def get_default_search_keys(): return [ 'Symbol', 'Description', '', # Revision 'Category', 'Symbol', 'Producer', 'OriginalSymbol', 'ProductInformationPage', 'Datasheet', 'Photo', ] def check_environment() -> bool: TME_API_TOKEN = os.environ.get('TME_API_TOKEN', None) TME_API_SECRET = os.environ.get('TME_API_SECRET', None) if not TME_API_TOKEN or not TME_API_SECRET: return False return True def setup_environment(force=False) -> bool: if not check_environment() or force: tme_api_settings = config_interface.load_file(settings.CONFIG_TME_API) os.environ['TME_API_TOKEN'] = tme_api_settings.get('TME_API_TOKEN', None) os.environ['TME_API_SECRET'] = tme_api_settings.get('TME_API_SECRET', None) return check_environment() # Based on TME API snippets mentioned in API documentation: https://developers.tme.eu/documentation/download # https://github.com/tme-dev/TME-API/blob/master/Python/call.py def tme_api_request(endpoint, tme_api_settings, params, api_host='https://api.tme.eu', format='json', **kwargs): TME_API_TOKEN = tme_api_settings.get('TME_API_TOKEN', None) TME_API_SECRET = tme_api_settings.get('TME_API_SECRET', None) params['Country'] = tme_api_settings.get('TME_API_COUNTRY', 'US') params['Language'] = tme_api_settings.get('TME_API_LANGUAGE', 'EN') if not TME_API_TOKEN and not TME_API_SECRET: TME_API_TOKEN = os.environ.get('TME_API_TOKEN', None) TME_API_SECRET = os.environ.get('TME_API_SECRET', None) if not TME_API_TOKEN and not TME_API_SECRET: from ..common.tools import cprint cprint('[INFO]\tWarning: Value not found for TME_API_TOKEN and/or TME_API_SECRET', silent=False) return None params['Token'] = TME_API_TOKEN params = collections.OrderedDict(sorted(params.items())) url = api_host + endpoint + '.' + format encoded_params = urllib.parse.urlencode(params, quote_via=urllib.parse.quote) signature_base = 'POST' + '&' + urllib.parse.quote(url, '') + '&' + urllib.parse.quote(encoded_params, '') hmac_value = hmac.new( TME_API_SECRET.encode(), signature_base.encode(), hashlib.sha1 ).digest() api_signature = base64.encodebytes(hmac_value).rstrip() params['ApiSignature'] = api_signature data = urllib.parse.urlencode(params).encode() headers = { "Content-type": "application/x-www-form-urlencoded", } return urllib.request.Request(url, data, headers) def tme_api_query(request: urllib.request.Request) -> dict: response = None try: data = urllib.request.urlopen(request).read().decode('utf8') except urllib.error.HTTPError: data = None if data: response = json.loads(data) return response def fetch_part_info(part_number: str) -> dict: def search_product(response): found = False index = 0 for product in response['Data']['ProductList']: if product['Symbol'] == part_number: found = True break index = index + 1 return found, index tme_api_settings = config_interface.load_file(settings.CONFIG_TME_API) params = {'SymbolList[0]': part_number} response = tme_api_query(tme_api_request('/Products/GetProducts', tme_api_settings, params)) if response is None or response['Status'] != 'OK': return {} # in the case if multiple parts returned # (for e.g. if we looking for NE555A we could have NE555A and NE555AB in the results) found = False index = 0 for product in response['Data']['ProductList']: if product['Symbol'] == part_number: found = True break index = index + 1 if not found: return {} part_info = response['Data']['ProductList'][index] part_info['Photo'] = "http:" + part_info['Photo'] part_info['ProductInformationPage'] = "http:" + part_info['ProductInformationPage'] part_info['category'] = part_info['Category'] part_info['subcategory'] = None # query the parameters params = {'SymbolList[0]': part_number} response = tme_api_query(tme_api_request('/Products/GetParameters', tme_api_settings, params)) # check if accidentally no data returned if response is None or response['Status'] != 'OK': return part_info found, index = search_product(response) if not found: return part_info part_info['parameters'] = {} for param in response['Data']['ProductList'][index]["ParameterList"]: part_info['parameters'][param['ParameterName']] = param['ParameterValue'] # query the prices params = {'SymbolList[0]': part_number, 'Curreny': 'USD'} response = tme_api_query(tme_api_request('/Products/GetPrices', tme_api_settings, params)) # check if accidentally no data returned if response is None or response['Status'] != 'OK': return part_info found, index = search_product(response) if not found: part_info['currency'] = 'USD' return part_info part_info['pricing'] = {} [pricing_key, qty_key, price_key, currency_key] = PRICING_MAP for price_break in response['Data']['ProductList'][index][pricing_key]: quantity = price_break[qty_key] price = price_break[price_key] part_info['pricing'][quantity] = price part_info['currency'] = response['Data'][currency_key] # Query the files associated to the product params = {'SymbolList[0]': part_number} response = tme_api_query(tme_api_request('/Products/GetProductsFiles', tme_api_settings, params)) # check if accidentally no products returned if response is None or response['Status'] != 'OK': return part_info found, index = search_product(response) if not found: return part_info for doc in response['Data']['ProductList'][index]['Files']['DocumentList']: if doc['DocumentType'] == 'DTE': part_info['Datasheet'] = 'http:' + doc['DocumentUrl'] break return part_info def test_api(check_content=False) -> bool: ''' Test method for API ''' setup_environment() test_success = True expected = { 'Description': 'Capacitor: ceramic; MLCC; 33pF; 50V; C0G; ±5%; SMD; 0402', 'Symbol': 'CL05C330JB5NNNC', 'Producer': 'SAMSUNG', 'OriginalSymbol': 'CL05C330JB5NNNC', 'ProductInformationPage': 'http://www.tme.eu/en/details/cl05c330jb5nnnc/mlcc-smd-capacitors/samsung/', 'Datasheet': 'http://www.tme.eu/Document/7da762c1dbaf553c64ad9c40d3603826/mlcc_samsung.pdf', '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', } test_part = fetch_part_info('CL05C330JB5NNNC') # Check for response if not test_part: test_success = False if not check_content: return test_success # Check content of response if test_success: for key, value in expected.items(): if test_part[key] != value: print(f'{test_part[key]} != {value}') test_success = False break return test_success ================================================ FILE: kintree/setup_inventree.py ================================================ import sys from .config import settings from .common.tools import cprint from .config import config_interface from .database import inventree_api, inventree_interface def setup_inventree(): SETUP_CATEGORIES = True SETUP_PARAMETERS = True def create_categories(parent, name, categories): category_pk, is_category_new = inventree_api.create_category(parent=parent, name=name) if is_category_new: cprint(f'[TREE]\tSuccess: Category "{name}" was added to InvenTree') else: cprint(f'[TREE]\tWarning: Category "{name}" already exists') if categories[name]: for cat in categories[name]: create_categories(parent=name, name=cat, categories=categories[name]) if SETUP_CATEGORIES or SETUP_PARAMETERS: cprint('\n[MAIN]\tStarting InvenTree setup', silent=settings.SILENT) # Load category configuration file categories = config_interface.load_file(settings.CONFIG_CATEGORIES)['CATEGORIES'] cprint('[MAIN]\tConnecting to Inventree', silent=settings.SILENT) inventree_connect = inventree_interface.connect_to_server() if not inventree_connect: sys.exit(-1) # Setup database for test inventree_api.set_inventree_db_test_mode() if SETUP_CATEGORIES: for category in categories.keys(): cprint(f'\n[MAIN]\tCreating categories in {category.upper()}') create_categories(parent=None, name=category, categories=categories) if SETUP_PARAMETERS: # Load parameter configuration file parameters = config_interface.load_file(settings.CONFIG_PARAMETERS) # cprint(parameters) cprint('\n[MAIN]\tLoading Parameters') for name, unit in parameters.items(): pk = inventree_api.create_parameter_template(name, unit) if pk > 0: cprint(f'[TREE]\tSuccess: Parameter "{name}" was added to InvenTree') else: cprint(f'[TREE]\tWarning: Parameter "{name}" already exists') if __name__ == '__main__': setup_inventree() ================================================ FILE: kintree_gui.py ================================================ import sys from kintree.kintree_gui import main if __name__ == '__main__': if len(sys.argv) > 1: main(view='browser') sys.exit() main(view='flet_app') ================================================ FILE: poetry.toml ================================================ [virtualenvs] in-project = true ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "kintree" version = "1.2.1" # placeholder description = "Fast part creation in KiCad and InvenTree" authors = ["eeintech "] maintainers = ["eeintech "] license = "GPL-3.0-or-later" readme = "README.md" homepage = "https://github.com/sparkmicro/Ki-nTree" repository = "https://github.com/sparkmicro/Ki-nTree" keywords = ["inventree", "kicad", "digikey", "mouser", "component", "part", "create"] [tool.poetry.dependencies] python = ">=3.9,<3.14" # digikey-api = "^1.0.0" digikey-api = { git = "https://github.com/hurricaneJoef/digikey-api.git", branch = "master" } setuptools = "^75.6.0" flet = "0.24.1" thefuzz = "^0.22.1" inventree = "^0.23.1" kiutils = "^1.4.8" mouser = "^0.1.6" multiprocess = "^0.70.17" pyyaml = "^6.0.2" validators = "^0.34.0" wrapt_timeout_decorator = "^1.5.1" cloudscraper = "^1.2.71" [tool.poetry.dev-dependencies] invoke = "^2.0.0" coveralls = "^3.3.1" [tool.poetry.scripts] kintree = 'kintree.kintree_gui:main' kintree_setup_inventree = 'kintree.setup_inventree:setup_inventree' [build-system] requires = ["poetry-core>=1.4.2"] build-backend = "poetry.core.masonry.api" ================================================ FILE: requirements.txt ================================================ cloudscraper==1.2.71 setuptools==75.2.0 https://github.com/hurricaneJoef/digikey-api/archive/refs/heads/master.zip Flet>=0.24.1,<=0.24.1 thefuzz>=0.19.0,<1.0 inventree>=0.23.1,<1.0 kiutils>=1.4.8,<2.0 mouser>=0.1.6,<1.0 multiprocess>=0.70.16,<0.71 PyYAML>=6.0.1,<7.0 validators>=0.19.0,<1.0 wrapt_timeout_decorator>=1.5.1,<2.0 ================================================ FILE: run_tests.py ================================================ import os import sys import kintree.config.settings as settings from kintree.common.tools import cprint, create_library, download_with_retry from kintree.config import config_interface from kintree.database import inventree_api, inventree_interface from kintree.kicad import kicad_interface from kintree.search import ( digikey_api, mouser_api, element14_api, lcsc_api, tme_api, snapeda_api, automationdirect_api, jameco_api, ) from kintree.setup_inventree import setup_inventree # SETTINGS # Enable API tests try: ENABLE_API = int(sys.argv[1]) except IndexError: ENABLE_API = 0 # Enable InvenTree tests ENABLE_INVENTREE = False # Enable KiCad tests ENABLE_KICAD = True # Set categories to test PART_CATEGORIES = [ 'Capacitors', 'Circuit Protections', 'Connectors', 'Crystals and Oscillators', 'Diodes', 'Inductors', 'Integrated Circuits', 'Mechanicals', 'Power Management', 'Resistors', 'RF', 'Transistors', ] # Enable tests on extra methods ENABLE_TEST_METHODS = True ### # Pretty test printing def pretty_test_print(message: str): cprint(message.ljust(65), end='') # Check result def check_result(status: str, new_part: bool) -> bool: # Build result success = False if (status == 'original') or (status == 'fake_alternate'): if new_part: success = True elif status == 'alternate_mpn': if not new_part: success = True else: pass return success # --- SETUP --- # Enable test mode settings.enable_test_mode() # Enable InvenTree and KiCad settings.set_enable_flag('inventree', True) settings.set_enable_flag('alternate', False) settings.set_enable_flag('kicad', True) # Load user configuration files settings.load_user_config() # Set path to test libraries test_library_path = os.path.join(settings.PROJECT_DIR, 'tests', 'TEST.kicad_sym') symbol_libraries_test_path = os.path.join(settings.PROJECT_DIR, 'tests', 'files', 'SYMBOLS') footprint_libraries_test_path = os.path.join(settings.PROJECT_DIR, 'tests', 'files', 'FOOTPRINTS', '') if ENABLE_API: # Disable Digi-Key API logging digikey_api.disable_api_logger() # Test Digi-Key API if 'Digi-Key' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tDigi-Key API Test') if not digikey_api.test_api(check_content=True): cprint('[ FAIL ]') cprint('[INFO]\tFailed to get Digi-Key API token, aborting.') else: cprint('[ PASS ]') # Test Mouser API if 'Mouser' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tMouser API Test') if not mouser_api.test_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') # Test Element14 API (with retry, to avoid the few false positives) if 'Element14' in settings.SUPPORTED_SUPPLIERS_API: for i in range(2): pretty_test_print('[MAIN]\tElement14 API Test') if not element14_api.test_api() or not element14_api.test_api(store_url='www.newark.com'): cprint('[ FAIL ]') else: cprint('[ PASS ]') break # Test LCSC API if 'LCSC' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tLCSC API Test') if not lcsc_api.test_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') # Test TME API if 'TME' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tTME API Test') if not tme_api.test_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') # Test AutomationDirect API if 'AutomationDirect' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tAutomationDirect API Test') if not automationdirect_api.test_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') # Test Jameco API if 'Jameco' in settings.SUPPORTED_SUPPLIERS_API: pretty_test_print('[MAIN]\tJameco API Test') if not jameco_api.test_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') # Test SnapEDA API methods pretty_test_print('[MAIN]\tSnapEDA API Test') if not snapeda_api.test_snapeda_api(): cprint('[ FAIL ]') else: cprint('[ PASS ]') cprint('\n-----') if ENABLE_INVENTREE: # Setup InvenTree pretty_test_print('\n[MAIN]\tSetting up Inventree') setup_inventree() cprint('\n-----') # Load test samples samples = config_interface.load_file(os.path.abspath( os.path.join('tests', 'test_samples.yaml'))) PART_TEST_SAMPLES = {} for category in PART_CATEGORIES: PART_TEST_SAMPLES.update({category: samples[category]}) # Store results exit_code = 0 kicad_results = {} inventree_results = {} # --- TESTS --- if __name__ == '__main__': if settings.ENABLE_TEST: if ENABLE_INVENTREE: pretty_test_print('\n[MAIN]\tConnecting to Inventree') inventree_connect = inventree_interface.connect_to_server() if inventree_connect: cprint('[ PASS ]') else: cprint('[ FAIL ]') if ENABLE_KICAD or ENABLE_INVENTREE: for category in PART_TEST_SAMPLES.keys(): cprint(f'\n[MAIN]\tCategory: {category.upper()}') # For last category, combine creation of KiCad and InvenTree parts last_category = False if ENABLE_KICAD and ENABLE_INVENTREE and category == list(PART_TEST_SAMPLES.keys())[-1]: last_category = True for number, status in PART_TEST_SAMPLES[category].items(): kicad_result = False inventree_result = False # Fetch supplier data supplier_info = inventree_interface.supplier_search( supplier='Digi-Key', part_number=number, test_mode=True, ) # Translate to form part_info = inventree_interface.translate_supplier_to_form( supplier='Digi-Key', part_info=supplier_info, ) # Stitch categories and parameters part_info.update({ 'category_tree': [supplier_info['category'], supplier_info['subcategory']], 'parameters': supplier_info['parameters'], 'Symbol': f'{category}:{number}', 'IPN': supplier_info['manufacturer_product_number'], }) # Update categories part_info['category_tree'] = inventree_interface.get_categories_from_supplier_data(part_info) # Needed for tests part_info['Template'] = part_info['category_tree'] # Display part to be tested pretty_test_print(f'[INFO]\tChecking "{number}" ({status})') if ENABLE_INVENTREE: # Adding part information to InvenTree categories = [None, None] new_part = False part_pk = 0 part_data = {} # Create part in InvenTree new_part, part_pk, part_data = inventree_interface.inventree_create( part_info=part_info, kicad=last_category, symbol=part_info['Symbol'], show_progress=False, enable_upload=True if number == 'BSS84-7-F' else False, ) inventree_result = check_result(status, new_part) pk_list = [data[0] for data in inventree_results.values()] if part_pk != 0 and part_pk not in pk_list: delete = True else: delete = False # Log results inventree_results.update({number: [part_pk, inventree_result, delete]}) if ENABLE_KICAD: if settings.AUTO_GENERATE_LIB: create_library( os.path.dirname(test_library_path), 'TEST', settings.symbol_template_lib, ) kicad_result, kicad_new_part, kicad_part_name = kicad_interface.inventree_to_kicad( part_data=part_info, library_path=test_library_path, show_progress=False, ) # Log result if number not in kicad_results.keys(): kicad_results.update({number: kicad_result}) # Combine KiCad and InvenTree for less verbose result = False if ENABLE_KICAD and ENABLE_INVENTREE: result = kicad_result and inventree_result else: result = kicad_result or inventree_result # Print live results if result: cprint('[ PASS ]') else: cprint('[ FAIL ]') exit_code = -1 if ENABLE_KICAD: cprint(f'[DBUG]\tkicad_result = {kicad_result}') cprint(f'[DBUG]\tkicad_new_part = {kicad_new_part}') cprint(f'[DBUG]\tkicad_part_name = {kicad_part_name}') if ENABLE_INVENTREE: cprint(f'[DBUG]\tinventree_result = {inventree_result}') cprint(f'[DBUG]\tnew_part = {new_part}') cprint(f'[DBUG]\tpart_pk = {part_pk}') # Disable datasheet download/upload after first part (to speed up testing) # settings.DATASHEET_UPLOAD = False if ENABLE_TEST_METHODS and ENABLE_INVENTREE: methods = [ 'Fuzzy category matching', 'Custom parts form', 'Digi-Key search missing part number', 'Load KiCad library paths', 'Add symbol library to user file', 'Add footprint library to user file', 'Add supplier category', 'Sync InvenTree and supplier categories', 'Download image/PDF method', 'Get category parameters', 'Add valid alternate supplier part using part ID', 'Add invalid alternate supplier part using part IPN', 'Save InvenTree settings', 'Load configuration files', 'Build InvenTree category tree (file, db and branch)', ] method_success = True # Line return cprint('') cprint('[MAIN]\tChecking untested methods'.ljust(65)) for method_idx, method_name in enumerate(methods): pretty_test_print(method_name) if method_idx == 0: # Fuzzy category matching part_info = { 'category_tree': ['Capacitors', 'Super',], } categories = tuple(inventree_interface.get_categories_from_supplier_data(part_info)) if not (categories[0] and categories[1]): method_success = False elif method_idx == 1: # Custom part form try: inventree_interface.translate_form_to_inventree(part_info, categories) # If the above function does not fail, it's a problem method_success = False except KeyError: pass part_info = { 'name': 'part_name', 'description': 'part_desc', 'revision': 'part_rev', 'keywords': 'part_key', 'supplier_name': 'part_supplier', 'supplier_part_number': 'part_sku', 'supplier_link': 'part_link', 'manufacturer_name': 'part_man', 'manufacturer_part_number': 'part_mpn', 'datasheet': 'part_data', 'image': 'part_image', 'IPN': 'part_mpn', } if not inventree_interface.translate_form_to_inventree(part_info, categories, is_custom=True): method_success = False elif method_idx == 2: # Digi-Key search missing part number search = inventree_interface.supplier_search(supplier='Digi-Key', part_number='') if search: method_success = False elif method_idx == 3: # Load KiCad library paths config_interface.load_library_path(settings.KICAD_CONFIG_PATHS, silent=True) symbol_libraries_paths = config_interface.load_libraries_paths(settings.KICAD_CONFIG_CATEGORY_MAP, symbol_libraries_test_path) footprint_libraries_paths = config_interface.load_footprint_paths(settings.KICAD_CONFIG_CATEGORY_MAP, footprint_libraries_test_path) if not (symbol_libraries_paths and footprint_libraries_paths): method_success = False elif method_idx == 4: # Add symbol library to user file add_symbol_lib = config_interface.add_library_path(user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP, category='category_test', symbol_library='symbol_library_test') if not add_symbol_lib: method_success = False elif method_idx == 5: # Add footprint library to user file add_footprint_lib = config_interface.add_footprint_library(user_config_path=settings.KICAD_CONFIG_CATEGORY_MAP, category='category_test', library_folder='footprint_folder_test') if not add_footprint_lib: method_success = False elif method_idx == 6: # Add supplier category categories = { 'Capacitors': {'Super': 'Super'} } add_category = config_interface.add_supplier_category(categories, settings.CONFIG_DIGIKEY_CATEGORIES) if not add_category: method_success = False elif method_idx == 7: # Sync InvenTree and Supplier categories sync_categories = config_interface.sync_inventree_supplier_categories(inventree_config_path=settings.CONFIG_CATEGORIES, supplier_config_path=settings.CONFIG_DIGIKEY_CATEGORIES) if not sync_categories: method_success = False elif method_idx == 8: test_image_urllib = 'https://media.digikey.com/Renders/Diodes%20Renders/31;%20SOD-123;%20;%202.jpg' test_image_requestslib = 'https://www.newark.com/productimages/standard/en_GB/GE2SOD12307-40.jpg' test_pdf_urllib = 'https://www.seielect.com/Catalog/SEI-CF_CFM.pdf' # Test different download methods for images if not download_with_retry(test_image_urllib, './image1.jpg', silent=True, filetype='Image'): print(' [1] ') method_success = False if not download_with_retry(test_image_requestslib, './image2.jpg', silent=True, filetype='Image'): print(' [2] ') method_success = False # Test PDF if not download_with_retry(test_pdf_urllib, './datasheet.pdf', silent=True, filetype='PDF'): print(' [3] ') method_success = False # Wrong folder if download_with_retry(test_pdf_urllib, './myfolder/datasheet.pdf', silent=True, filetype='PDF'): print(' [4] ') method_success = False # Test erroneous URL if download_with_retry('http', '', silent=True): print(' [5] ') method_success = False # Test empty URL if download_with_retry('', '', silent=True): print(' [6] ') method_success = False elif method_idx == 9: # Test InvenTree category parameters if inventree_api.get_category_parameters(1): method_success = False elif method_idx == 10: # Test manufacturer and supplier alternates using Part ID part_info = { "datasheet": "https://search.murata.co.jp/Ceramy/image/img/A01X/G101/ENG/GRM155R71C104KA88-01.pdf", "manufacturer_name": "Murata Electronics", "manufacturer_part_number": "GRM155R71C104KA88D", "supplier_link": "https://www.digikey.com/en/products/detail/murata-electronics/GRM155R71C104KA88D/675947", "supplier_name": "Digi-Key", "supplier_part_number": "490-3261-1-ND", "name": "", "description": "", "revision": "", "keywords": "", "IPN": "", "image": "", "parameters": {}, "pricing": {}, } if not inventree_interface.inventree_create_alternate(part_info=part_info, part_id='1', show_progress=False, ): method_success = False elif method_idx == 11: # Test manufacturer and supplier alternates using Part IPN if inventree_interface.inventree_create_alternate(part_info=part_info, part_ipn='CAP-000001-00', show_progress=False, ): method_success = False elif method_idx == 12: # Save InvenTree settings if not config_interface.save_inventree_user_settings( enable=True, server='http://127.0.0.1:8000', username='admin', password='admin', enable_proxy=False, proxies={}, datasheet_upload=True, user_config_path=settings.INVENTREE_CONFIG, pricing_upload=True, ): method_success = False elif method_idx == 13: # Select one configuration file element14_config = os.path.join( settings.USER_SETTINGS['USER_FILES'], 'element14_config.yaml', ) # Delete the user configuration file os.remove(element14_config) # Try to load this file if config_interface.load_file(element14_config): method_success = False if method_success: # Load user configuration files if not settings.load_user_config(): method_success = False if method_success: # Load configuration files with incorrect paths if config_interface.load_user_config_files('', ''): method_success = False elif method_idx == 14: # Reload categories from file cat_from_file = inventree_interface.build_category_tree(reload=False) if isinstance(cat_from_file, type(list)): print(f'{type(cat_from_file)} != list') method_success = False if method_success: # Reload categories from InvenTree database cat_from_db = inventree_interface.build_category_tree(reload=True) if len(cat_from_db) != len(cat_from_file): print(f'{len(cat_from_db)} != {len(cat_from_file)}') method_success = False if method_success: # Reload category branch cat_branch = inventree_interface.build_category_tree(category='Crystals and Oscillators') if len(cat_branch) != 3: print(f'{len(cat_branch)} != 3') method_success = False if method_success: cprint('[ PASS ]') else: cprint('[ FAIL ]') exit_code = -1 break # Line return cprint('') sys.exit(exit_code) ================================================ FILE: setup.cfg ================================================ [flake8] ignore = # - W191 - indentation contains tab # W191, # - W293 - blank lines contain whitespace W293, W605, # - E501 - line too long (82 characters) E501, E722, # - C901 - function is too complex C901, # - N802 - function name should be lowercase N802, # - N806 - variable should be lowercase N806, N812, F824, exclude = .git,__pycache__,*/migrations/*,*/lib/*,*/bin/*,*/media/*,*/static/*,*ci_*.py* max-complexity = 20 ================================================ FILE: tasks.py ================================================ import webbrowser from kintree.common.tools import cprint from invoke import UnexpectedExit, task @task def install(c, is_install=True): """ Install Ki-nTree dependencies """ if is_install: cprint('[MAIN]\tInstalling required dependencies') c.run('pip install -U wheel', hide='out') else: cprint('[MAIN]\tUpdating required dependencies') c.run('pip install -U -r requirements.txt', hide='out') @task def update(c): """ Update Ki-nTree dependencies """ install(c, is_install=False) @task def clean(c): """ Clean project folder """ cprint('[MAIN]\tCleaning project directory') try: c.run('find . -name __pycache__ | xargs rm -r', hide='err') except UnexpectedExit: pass try: c.run('rm .coverage', hide='err') except UnexpectedExit: pass try: c.run('rm .coverage.*', hide='err') except UnexpectedExit: pass try: c.run('rm -r dist/ build/ htmlcov', hide='err') except UnexpectedExit: pass @task(pre=[clean]) def build(c): """ Build Ki-nTree into dist/wheel """ try: c.run('pip show poetry', hide=True) except UnexpectedExit: c.run('pip install -U poetry', hide=True) cprint('[MAIN]\tBuilding Ki-nTree GUI into "dist" directory') c.run('poetry build', hide=True) @task def setup_inventree(c): """ Setup InvenTree server """ c.run('python -m kintree.setup_inventree') @task def coverage_report(c, open_browser=True): """ Show coverage report """ cprint('[MAIN]\tBuilding coverage report') c.run('coverage report') c.run('coverage html') if open_browser: webbrowser.open('htmlcov/index.html', new=2) @task def test(c, enable_api=0): """ Run Ki-nTree tests """ try: c.run('pip show coverage', hide=True) except UnexpectedExit: c.run('pip install -U coverage', hide=True) cprint('[MAIN]\tRunning tests using coverage\n-----') # Start InvenTree server c.run('cd InvenTree/ && inv server && cd ..', asynchronous=True) c.run('sleep 15') # Copy test files c.run('cp -r tests/ kintree/') # Run Tests run_tests = c.run(f'coverage run run_tests.py {enable_api}') if run_tests.exited == 0: coverage_report(c, open_browser=False) @task def python_badge(c): """ Make badge for supported versions of Python """ cprint('[MAIN]\tInstall pybadges') c.run('pip install pybadges pip-autoremove', hide=True) cprint('[MAIN]\tCreate badge') c.run('python -m pybadges --left-text="python" --right-text="3.9 | 3.10 | 3.11 | 3.12" ' '--whole-link="https://www.python.org/" --browser --embed-logo ' '--logo="https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/python.svg"') cprint('[MAIN]\tUninstall pybadges') c.run('pip-autoremove pybadges -y', hide=True) c.run('pip uninstall pip-autoremove -y', hide=True) @task def style(c): """ Run PEP style checks against Ki-nTree sourcecode """ c.run('pip install -U flake8', hide=True) print("Running PEP style checks...") c.run('flake8 --extend-ignore W503 \ tasks.py run_tests.py kintree_gui.py kintree/kintree_gui.py kintree/setup_inventree.py \ kintree/common/ kintree/config/ kintree/database/ kintree/kicad/*.py kintree/search/*.py \ kintree/gui/gui.py kintree/gui/views/*.py') @task def gui(c, browser=False): """ Open GUI in either app or browser mode """ if browser: c.run('python -m kintree_gui b') return c.run('python -m kintree_gui') ================================================ FILE: tests/files/FOOTPRINTS/RF.pretty/Skyworks_SKY13575_639LF.kicad_mod ================================================ (module "Skyworks_SKY13575_639LF" (layer F.Cu) (tedit 5D6EEFCB) (descr "http://www.skyworksinc.com/uploads/documents/SKY13575_639LF_203270D.pdf") (tags "Skyworks") (attr smd) (fp_text reference "REF**" (at 0 -2.45) (layer F.SilkS) (effects (font (size 1 1) (thickness 0.15))) ) (fp_text value "Skyworks_SKY13575_639LF" (at -0.01 2.01) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15))) ) (fp_line (start -0.96 -0.89) (end -0.71 -0.89) (layer F.SilkS) (width 0.12)) (fp_text user "%R" (at 0 -1.55) (layer F.Fab) (effects (font (size 0.5 0.5) (thickness 0.075))) ) (fp_line (start 1.22 -1.22) (end -1.22 -1.22) (layer F.CrtYd) (width 0.05)) (fp_line (start 1.22 1.22) (end 1.22 -1.22) (layer F.CrtYd) (width 0.05)) (fp_line (start -1.22 1.22) (end 1.22 1.22) (layer F.CrtYd) (width 0.05)) (fp_line (start -1.22 -1.22) (end -1.22 1.22) (layer F.CrtYd) (width 0.05)) (fp_line (start -0.8 -0.4) (end -0.8 0.8) (layer F.Fab) (width 0.1)) (fp_line (start -0.4 -0.8) (end -0.8 -0.4) (layer F.Fab) (width 0.1)) (fp_line (start 0.8 -0.8) (end -0.4 -0.8) (layer F.Fab) (width 0.1)) (fp_line (start 0.8 0.8) (end 0.8 -0.8) (layer F.Fab) (width 0.1)) (fp_line (start -0.8 0.8) (end 0.8 0.8) (layer F.Fab) (width 0.1)) (pad "2" smd rect (at -0.772 -0.2) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "3" smd rect (at -0.772 0.2) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "9" smd rect (at 0.772 0.2) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "10" smd rect (at 0.772 -0.2) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "6" smd rect (at 0 0.772 90) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "13" smd rect (at 0 -0.772 90) (size 0.4 0.2) (layers "F.Cu" "F.Paste" "F.Mask")) (pad "4" smd custom (at -0.866 0.602) (size 0.2 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "1" smd custom (at -0.866 -0.601) (size 0.2 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "5" smd custom (at -0.4 0.871) (size 0.19 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "7" smd custom (at 0.4 0.87) (size 0.19 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "12" smd custom (at 0.398 -0.87) (size 0.19 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "14" smd custom (at -0.4 -0.869) (size 0.19 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "8" smd custom (at 0.871 0.6) (size 0.2 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "11" smd custom (at 0.872 -0.6) (size 0.2 0.2) (layers "F.Cu" "F.Paste" "F.Mask") (zone_connect 2) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "15" smd custom (at 0 0) (size 0.29 0.29) (layers "F.Cu" "F.Mask") (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (pad "" smd rect (at 0.185 -0.185) (size 0.29 0.29) (layers "F.Paste")) (pad "" smd rect (at 0.185 0.185) (size 0.29 0.29) (layers "F.Paste")) (pad "" smd rect (at -0.185 0.185) (size 0.29 0.29) (layers "F.Paste")) (pad "" smd custom (at -0.17 -0.17) (size 0.1 0.1) (layers "F.Paste") (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (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) ) (width 0)) )) (model "${KISYS3DMOD}/RF.3dshapes/Skyworks_SKY13575_639LF.wrl" (at (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)) ) ) ================================================ FILE: tests/files/FOOTPRINTS/RF.pretty/Skyworks_SKY65404-31.kicad_mod ================================================ (module Skyworks_SKY65404-31 (layer F.Cu) (tedit 5C4622B0) (descr http://www.skyworksinc.com/uploads/documents/SKY65404_31_201512K.pdf) (tags Skyworks) (attr smd) (fp_text reference REF** (at 0 -1.75) (layer F.SilkS) (effects (font (size 1 1) (thickness 0.15))) ) (fp_text value Skyworks_SKY65404-31 (at 0 1.85) (layer F.Fab) (effects (font (size 1 1) (thickness 0.15))) ) (fp_line (start -1.15 -1) (end 1.15 -1) (layer F.CrtYd) (width 0.05)) (fp_line (start -1.15 1) (end -1.15 -1) (layer F.CrtYd) (width 0.05)) (fp_line (start 1.15 1) (end -1.15 1) (layer F.CrtYd) (width 0.05)) (fp_line (start 1.15 -1) (end 1.15 1) (layer F.CrtYd) (width 0.05)) (fp_text user %R (at 0 0 270) (layer F.Fab) (effects (font (size 0.5 0.5) (thickness 0.075))) ) (fp_line (start -0.75 -0.375) (end -0.375 -0.75) (layer F.Fab) (width 0.1)) (fp_line (start -0.75 0.75) (end -0.75 -0.375) (layer F.Fab) (width 0.1)) (fp_line (start 0.75 0.75) (end -0.75 0.75) (layer F.Fab) (width 0.1)) (fp_line (start 0.75 -0.75) (end 0.75 0.75) (layer F.Fab) (width 0.1)) (fp_line (start -0.375 -0.75) (end 0.75 -0.75) (layer F.Fab) (width 0.1)) (fp_line (start -0.75 0.85) (end 0.75 0.85) (layer F.SilkS) (width 0.12)) (fp_line (start 0 -0.85) (end 0.75 -0.85) (layer F.SilkS) (width 0.12)) (pad 7 smd custom (at 0 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (zone_connect 0) (options (clearance outline) (anchor rect)) (primitives (gr_poly (pts (xy 0.35 -0.6) (xy 0.35 0) (xy 0.35 0.6) (xy 0.1 0.6) (xy 0.1 0.65) (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) (xy -0.1 -0.6) (xy -0.1 -0.65) (xy 0.1 -0.65) (xy 0.1 -0.6)) (width 0)) )) (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)) (pad 5 smd roundrect (at 0.725 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) (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)) (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)) (pad 2 smd roundrect (at -0.725 0) (size 0.35 0.25) (layers F.Cu F.Paste F.Mask) (roundrect_rratio 0.25)) (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)) (model ${KISYS3DMOD}/RF.3dshapes/Skyworks_SKY65404-31.wrl (at (xyz 0 0 0)) (scale (xyz 1 1 1)) (rotate (xyz 0 0 0)) ) ) ================================================ FILE: tests/files/SYMBOLS/TEST.kicad_sym ================================================ (kicad_symbol_lib (version 20220914) (generator kicad_symbol_editor) (symbol "C0402C100J3GACTU" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 4.064 0) (effects (font (size 1.524 1.524))) ) (property "Value" "C0402C100J3GACTU" (at 0 -11.43 0) (effects (font (size 1.524 1.524)) hide) ) (property "Footprint" "Capacitors:C0402" (at 0 -26.67 0) (effects (font (size 1.524 1.524)) hide) ) (property "Datasheet" "http://ksim.kemet.com/Plots/SpicePlots.aspx" (at 0 -13.97 0) (effects (font (size 1.524 1.524)) hide) ) (property "Supplier Part Number" "399-7746-1-ND" (at 0 -16.51 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer" "KEMET" (at 0 -19.05 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer Part Number" "C0402C100J3GACTU" (at 0 -21.59 0) (effects (font (size 1.524 1.524)) hide) ) (property "Description" "CAP 10PF 25V C0G/NP0 0402" (at 0 -24.13 0) (effects (font (size 1.524 1.524)) hide) ) (property "Capacitance (Farad)" "10pF" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" " ±5%" (at 5.08 -3.81 0) (effects (font (size 1.27 1.27)) hide) ) (property "Voltage Rated (Volt)" "25V" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Variant" "dnp" (at 0 -29.21 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "10pF 25V 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "CAP CER 10PF 25V C0G/NP0 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "C0402C100J3GACTU_0_1" (polyline (pts (xy -1.27 0) (xy -1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -0.889 1.905) (xy -0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 0.889 1.905) (xy 0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.905 0) (xy 2.54 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "C0402C100J3GACTU_1_1" (pin passive line (at -3.81 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 3.81 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) (symbol "C0402C103K8RACTU" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 4.064 0) (effects (font (size 1.524 1.524))) ) (property "Value" "C0402C103K8RACTU" (at 0 -11.43 0) (effects (font (size 1.524 1.524)) hide) ) (property "Footprint" "Capacitors:C0402" (at 0 -26.67 0) (effects (font (size 1.524 1.524)) hide) ) (property "Datasheet" "https://api.kemet.com/component-edge/download/datasheet/C0402C103K8RACTU.pdf" (at 0 -13.97 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer Part Number" "C0402C103K8RACTU" (at 0 -16.51 0) (effects (font (size 1.524 1.524)) hide) ) (property "Distributors" "Digi-Key" (at 0 -19.05 0) (effects (font (size 1.524 1.524)) hide) ) (property "Distributors References" "399-7759-1-ND" (at 0 -21.59 0) (effects (font (size 1.524 1.524)) hide) ) (property "Description" "CAP CER 10nF 10V X7R 0402" (at 0 -24.13 0) (effects (font (size 1.524 1.524)) hide) ) (property "Capacitance (Farad)" "10nF" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "±10%" (at 7.62 -3.81 0) (effects (font (size 1.27 1.27)) hide) ) (property "Voltage Rated (Volt)" "10V" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Variant" "dnp" (at 0 -29.21 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "10nF 10V 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "CAP CER 10nF 10V X7R 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "C0402C103K8RACTU_0_1" (polyline (pts (xy -1.27 0) (xy -1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -0.889 1.905) (xy -0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 0.889 1.905) (xy 0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.905 0) (xy 2.54 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "C0402C103K8RACTU_1_1" (pin passive line (at -3.81 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 3.81 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) (symbol "GRM155R70J105KA12J" (pin_numbers hide) (pin_names (offset 0) hide) (in_bom yes) (on_board yes) (property "Reference" "C" (at 0 4.064 0) (effects (font (size 1.524 1.524))) ) (property "Value" "GRM155R70J105KA12J" (at 0 -11.43 0) (effects (font (size 1.524 1.524)) hide) ) (property "Footprint" "Capacitors:C0402" (at 0 -26.67 0) (effects (font (size 1.524 1.524)) hide) ) (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) (effects (font (size 1.524 1.524)) hide) ) (property "Supplier Part Number" "490-13339-1-ND" (at 0 -16.51 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer" "Murata Electronics" (at 0 -19.05 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer Part Number" "GRM155R70J105KA12J" (at 0 -21.59 0) (effects (font (size 1.524 1.524)) hide) ) (property "Description" "CAP CER 1UF 6.3V X7R 0402" (at 0 -24.13 0) (effects (font (size 1.524 1.524)) hide) ) (property "Capacitance (Farad)" "1µF" (at 0 -3.81 0) (effects (font (size 1.27 1.27))) ) (property "Tolerance (%)" "±10%" (at 5.08 -3.81 0) (effects (font (size 1.27 1.27)) hide) ) (property "Voltage Rated (Volt)" "6.3V" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) ) (property "Variant" "dnp" (at 0 -29.21 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_keywords" "1uF 6.3V 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "CAP CER 1UF 6.3V X7R 0402" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "GRM155R70J105KA12J_0_1" (polyline (pts (xy -1.27 0) (xy -1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy -0.889 1.905) (xy -0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 0.889 1.905) (xy 0.889 -1.905) ) (stroke (width 0.254) (type default)) (fill (type none)) ) (polyline (pts (xy 1.27 0) (xy 1.016 0) ) (stroke (width 0) (type default)) (fill (type none)) ) (polyline (pts (xy 1.905 0) (xy 2.54 0) ) (stroke (width 0) (type default)) (fill (type none)) ) ) (symbol "GRM155R70J105KA12J_1_1" (pin passive line (at -3.81 0 0) (length 2.54) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 3.81 0 180) (length 2.54) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) ) ) (symbol "HTSH-110-01-F-DV-007-K" (pin_names (offset 1.016) hide) (in_bom yes) (on_board yes) (property "Reference" "J" (at 0 15.24 0) (effects (font (size 1.524 1.524))) ) (property "Value" "HTSH-110-01-F-DV-007-K" (at 0 -29.21 0) (effects (font (size 1.524 1.524)) hide) ) (property "Footprint" "Connectors:FTSH-110-01-F-DV-007-K" (at 0 -44.45 0) (effects (font (size 1.524 1.524)) hide) ) (property "Datasheet" "https://s3.amazonaws.com/catalogspreads-pdf/PAGE119%20.100%20SFH11%20SERIES%20FEMALE%20HDR%20ST%20RA.pdf" (at 0 -31.75 0) (effects (font (size 1.524 1.524)) hide) ) (property "Supplier Part Number" "SAM11264-ND" (at 0 -34.29 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer" "Samtec Inc." (at 0 -36.83 0) (effects (font (size 1.524 1.524)) hide) ) (property "Manufacturer Part Number" "FTSH-110-01-F-DV-007-K" (at 0 -39.37 0) (effects (font (size 1.524 1.524)) hide) ) (property "Description" "CONN HEADER MALE 20POS .05\" (1.27mm) Keying Shroud" (at 0 -41.91 0) (effects (font (size 1.524 1.524)) hide) ) (property "Variant" "dnp" (at 0 -46.99 0) (effects (font (size 1.524 1.524)) hide) ) (property "ki_keywords" "HEADER FEMALE 20POS RA" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (property "ki_description" "CONN HEADER FMALE 20PS .1\" R/A AU" (at 0 0 0) (effects (font (size 1.27 1.27)) hide) ) (symbol "HTSH-110-01-F-DV-007-K_0_1" (rectangle (start -5.08 12.7) (end 5.08 -12.7) (stroke (width 0) (type default)) (fill (type background)) ) ) (symbol "HTSH-110-01-F-DV-007-K_1_1" (pin passive line (at -10.16 11.43 0) (length 5.08) (name "1" (effects (font (size 1.27 1.27)))) (number "1" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 1.27 180) (length 5.08) (name "10" (effects (font (size 1.27 1.27)))) (number "10" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 -1.27 0) (length 5.08) (name "11" (effects (font (size 1.27 1.27)))) (number "11" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 -1.27 180) (length 5.08) (name "12" (effects (font (size 1.27 1.27)))) (number "12" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 -3.81 0) (length 5.08) (name "13" (effects (font (size 1.27 1.27)))) (number "13" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 -3.81 180) (length 5.08) (name "14" (effects (font (size 1.27 1.27)))) (number "14" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 -6.35 0) (length 5.08) (name "15" (effects (font (size 1.27 1.27)))) (number "15" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 -6.35 180) (length 5.08) (name "16" (effects (font (size 1.27 1.27)))) (number "16" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 -8.89 0) (length 5.08) (name "17" (effects (font (size 1.27 1.27)))) (number "17" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 -8.89 180) (length 5.08) (name "18" (effects (font (size 1.27 1.27)))) (number "18" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 -11.43 0) (length 5.08) (name "19" (effects (font (size 1.27 1.27)))) (number "19" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 11.43 180) (length 5.08) (name "2" (effects (font (size 1.27 1.27)))) (number "2" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 -11.43 180) (length 5.08) (name "20" (effects (font (size 1.27 1.27)))) (number "20" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 8.89 0) (length 5.08) (name "3" (effects (font (size 1.27 1.27)))) (number "3" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 8.89 180) (length 5.08) (name "4" (effects (font (size 1.27 1.27)))) (number "4" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 6.35 0) (length 5.08) (name "5" (effects (font (size 1.27 1.27)))) (number "5" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 6.35 180) (length 5.08) (name "6" (effects (font (size 1.27 1.27)))) (number "6" (effects (font (size 1.27 1.27)))) ) (pin passive line (at 10.16 3.81 180) (length 5.08) (name "8" (effects (font (size 1.27 1.27)))) (number "8" (effects (font (size 1.27 1.27)))) ) (pin passive line (at -10.16 1.27 0) (length 5.08) (name "9" (effects (font (size 1.27 1.27)))) (number "9" (effects (font (size 1.27 1.27)))) ) ) ) ) ================================================ FILE: tests/files/digikey_config.yaml ================================================ EXTRA_FIELDS: null SEARCH_DATASHEET: null SEARCH_DESCRIPTION: null SEARCH_KEYWORDS: null SEARCH_MANUFACTURER: null SEARCH_MPN: null SEARCH_NAME: null SEARCH_REVISION: null SEARCH_SKU: null SEARCH_SUPPLIER_URL: null SUPPLIER_INVENTREE_NAME: Digi-Key ================================================ FILE: tests/files/inventree_dev.yaml ================================================ DATASHEET_UPLOAD: true ENABLE: true ENABLE_PROXY: false PASSWORD: !!binary | WVdSdGFXND0= SERVER_ADDRESS: http://127.0.0.1:8000/ USERNAME: admin PROXIES: null ================================================ FILE: tests/files/kicad_map.yaml ================================================ KICAD_FOOTPRINTS: Capacitors: - Capacitors Circuit Protections: - Fuses Connectors: - Connectors Crystals and Oscillators: - Crystals Diodes: - Diodes Inductors: - Inductors Integrated Circuits: - Package_SO - Package_DFN - Package_QFP - Package_QFN Mechanicals: - Mechanicals - Switches Miscellaneous: - Misc Power Management: - Package_SO - Package_GEN - Package_DFN RF: - Antennas Resistors: - Resistors Transistors: - Package_SO KICAD_LIBRARIES: Capacitors: - Capacitors Circuit Protections: - Circuit_Protections Connectors: - Connectors Crystals and Oscillators: - Crystals_Oscillators Diodes: - Diodes Inductors: - Inductors Integrated Circuits: - Integrated_Circuits Mechanicals: - Mechanicals Miscellaneous: - Miscellaneous Power Management: - Power_Management RF: - RF Resistors: - Resistors Transistors: - Transistors KICAD_TEMPLATES: Capacitors: Aluminium: capacitor-polarized Ceramic: capacitor Default: capacitor Polymer: capacitor-polarized Super Capacitors: capacitor-polarized Tantalum: capacitor-polarized Circuit Protections: Default: protection-unidir Fuse: fuse TVS: protection-unidir Connectors: Default: connector Crystals and Oscillators: Crystal 2P: crystal-2p Default: crystal-2p Oscillator 4P: oscillator-4p Diodes: Default: diode-standard LED: diode-led Schottky: diode-schottky Standard: diode-standard Zener: diode-zener Inductors: Default: inductor Ferrite Bead: ferrite-bead Power: inductor Integrated Circuits: Default: integrated-circuit EEPROM SOT23: eeprom-sot23 Mechanicals: Default: default Power Management: Default: integrated-circuit RF: Default: integrated-circuit Resistors: Default: resistor Surface Mount: resistor-sm Through Hole: resistor Transistors: Default: transistor-nfet N-Channel FET: transistor-nfet NPN: transistor-npn P-Channel FET: transistor-pfet PNP: transistor-pnp ================================================ FILE: tests/test_samples.yaml ================================================ Capacitors: ## 0402 0.1u 16V X7R 0402B104K160CT: original # Equivalent CL05B104KO5NNNC: alternate_mpn # 'Fake' Equivalent (Temperature Grade) C0402C104K4PACTU: fake_alternate # Aluminum EEE-HA1A101WP: original # Tantalum T491A106K010AT: original # Aluminum-Polymer RSA0J331MCN1GS: original # Tantalum-Polymer 10TPE68M: original # Super-Capacitor CPH3225A: original Circuit Protections: # TVS SMAJ5.0CA-13-F: original # ESD ESDALC14-1BF4: original # Fuse 0603SFP150F/32-2: original # PTC 0ZCM0005FF2G: original Connectors: # Board-to-board 10144517-061802LF: original # Interface, microSD DM3CS-SF: original # Interface, USB Type-C USB4085-GF-A: original # Interface, Audio Jack SJ-3524-SMT-TR: original # Interface, Ethernet J3011G21DNLT: original # Coaxial H.FL-R-SMT(C)(10): original # FPC 0522713069: original # Header Male (Power) 0533980271: original # Header Female 20021321-00010C4LF: original Crystals and Oscillators: # 2-pin 32.768kHz Crystal ABS07-120-32.768KHZ-T: original # 4-pin 40MHz Oscillator KC2520C40.0000C2YE00: original Diodes: # General Purpose 1N4148W-7-F: original # Zener MMSZ5231B-7-F: original # LED LTST-C191KGKT: original # Schottky PMEG10010ELRX: original Inductors: # Power NRS5030T4R7MMGJV: original # Ferrite Bead BLM18AG601SN1D: original Integrated Circuits: # Logic SN74LVC2T45DCTR: original # Memory MX25R6435FM2IL0: original # Microcontroller STM32F730R8T6: original # Interface MAX9867ETJ+T: original # Sensor BME280: original Mechanicals: # Switch TL1105SF160Q: original Power Management: # Buck, Adjustable AOZ1282CI: original # Buck, Fixed SC189CULTRT: original # Boost, Fixed TPS61221DCKR: original # LDO, Adjustable LP3982IMM-ADJ/NOPB: original # LDO, Fixed AP2210K-3.3TRG1: original Resistors: # 10K/5% ### Surface Mount ## 0603 RK73B1JTTD103J: original # Equivalent ERJ-3GEYJ103V: alternate_mpn # 'Fake' Equivalent (Tolerance) RC0603FR-0710KL: fake_alternate ## 0402 RC0402FR-07100RL: original # Equivalent CRCW0402100RFKEDC: alternate_mpn # 'Fake' Equivalent (Power) ERJ-2GEJ101X: fake_alternate ### Through Hole CF14JT10K0: original # Equivalent CFR16J10K: alternate_mpn # 'Fake' Equivalent (Temperature) CBT25J10K: fake_alternate ### Array # ('original', 'EXB-28V103JX'), # # Equivalent # ('alternate_mpn', 'CAT10-103J4LF'), # # 'Fake' Equivalent (Power) # ('fake_alternate', 'EXB-N8V103JX'), # ### Potentiometer # ('original', '3590S-2-103L'), # ### NTC # ('original', 'NCP15XH103J03RC'), RF: # Filter (Balun) 2450BM14E0003001T: original Transistors: # NPN MMBT3904-7-F: original # PNP MMBT3906TT1G: original # N-Channel FET 2N7002-7-F: original # P-Channel FET BSS84-7-F: original