├── .github ├── FUNDING.yml └── workflows │ └── update_components.yaml ├── .gitignore ├── LCSC-API.md ├── LICENSE ├── README.md ├── jlcparts ├── attributes.py ├── common.py ├── datatables.py ├── descriptionAttributes.py ├── jlcpcb.py ├── lcsc.py ├── migrate.py ├── partLib.py └── ui.py ├── setup.py ├── test ├── testLibraryA.json ├── testLibraryB.json └── testParts.csv └── web ├── .gitignore ├── .modernizrrc ├── .vscode └── launch.json ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── brokenimage.svg ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── favicon.svg ├── index.html ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── mstile-70x70.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── src ├── app.js ├── componentTable.js ├── corsBridge.js ├── db.js ├── history.js ├── index.js ├── jlc.js ├── serviceWorker.js ├── sortableTable.js ├── tailwind.css └── units.js └── tailwind.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: yaqwsx 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: yaqwsx 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /.github/workflows/update_components.yaml: -------------------------------------------------------------------------------- 1 | name: "Update component database" 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 3 * * *' 7 | jobs: 8 | build_and_update: 9 | name: "Update component database and frontend" 10 | runs-on: ubuntu-22.04 11 | environment: github-pages 12 | steps: 13 | - name: Maximize build space 14 | uses: easimon/maximize-build-space@master 15 | with: 16 | root-reserve-mb: 512 17 | swap-size-mb: 1024 18 | remove-dotnet: 'true' 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Install dependencies 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y --no-install-recommends \ 25 | python3 python3-pip nodejs npm wget zip unzip p7zip-full 26 | sudo pip3 install requests click 27 | - name: Build frontend 28 | run: | 29 | cd web 30 | if [ "$GITHUB_REPOSITORY" = 'yaqwsx/jlcparts-dev' ]; then 31 | export BASEURL=https://jlcparts-dev.honzamrazek.cz 32 | else 33 | export BASEURL=https://yaqwsx.github.io/jlcparts 34 | fi 35 | npm install --silent 36 | NODE_ENV=production PUBLIC_URL=${BASEURL} npm run build 37 | if [ $GITHUB_REPOSITORY = 'yaqwsx/jlcparts-dev' ]; then 38 | echo 'jlcparts-dev.honzamrazek.cz' > build/CNAME 39 | fi 40 | touch .nojekyll 41 | - name: Update database 42 | env: # Or as an environment variable 43 | LCSC_KEY: ${{ secrets.LCSC_KEY }} 44 | LCSC_SECRET: ${{ secrets.LCSC_SECRET }} 45 | JLCPCB_KEY: ${{ secrets.JLCPCB_KEY }} 46 | JLCPCB_SECRET: ${{ secrets.JLCPCB_SECRET }} 47 | run: | 48 | set -x 49 | sudo pip3 install -e . 50 | 51 | wget -q https://yaqwsx.github.io/jlcparts/data/cache.zip 52 | for seq in $(seq -w 01 30); do 53 | wget -q https://yaqwsx.github.io/jlcparts/data/cache.z$seq || true 54 | done 55 | 56 | 7z x cache.zip 57 | for seq in $(seq -w 01 30); do 58 | rm cache.z$seq || true 59 | done 60 | 61 | jlcparts fetchtable parts.csv 62 | 63 | jlcparts getlibrary --age 10000 \ 64 | --limit 1000 \ 65 | parts.csv cache.sqlite3 66 | jlcparts updatepreferred cache.sqlite3 67 | jlcparts buildtables --jobs 0 \ 68 | --ignoreoldstock 120 \ 69 | cache.sqlite3 web/build/data 70 | 71 | rm -f web/build/data/cache.z* 72 | zip -s 50m web/build/data/cache.zip cache.sqlite3 73 | - name: Tar artifact # Artifact are case insensitive, this is workaround 74 | run: tar -czf web_build.tar.gz web/build/ 75 | - name: Upload artifact 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: web_build 79 | path: web_build.tar.gz 80 | retention-days: 14 81 | - name: Upload table 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: component_table 85 | path: parts.csv 86 | retention-days: 14 87 | deploy: 88 | name: "Deploy" 89 | runs-on: ubuntu-22.04 90 | needs: build_and_update 91 | if: github.ref == 'refs/heads/master' 92 | steps: 93 | - name: Checkout # Required for GH-pages deployment 94 | uses: actions/checkout@v3 95 | - name: "Download web" 96 | uses: actions/download-artifact@v4 97 | with: 98 | name: web_build 99 | - name: Untar artifact 100 | run: tar -xzf web_build.tar.gz 101 | - name: Deploy to GH Pages 102 | uses: JamesIves/github-pages-deploy-action@v4.4.3 103 | with: 104 | branch: gh-pages 105 | folder: web/build 106 | single-commit: true 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | .idea 3 | *.zip 4 | *.z* 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # Cython debug symbols 143 | cython_debug/ 144 | 145 | # Ignore sandbox 146 | sandbox 147 | -------------------------------------------------------------------------------- /LCSC-API.md: -------------------------------------------------------------------------------- 1 | # LCSC API 2 | 3 | - to get a product page based on LCSC number use GET request on 4 | `https://lcsc.com/api/global/additional/search?q=`. You get a 5 | JSON with URL of the product. 6 | - to get a product options based on LSCS number use POST request to 7 | `https://lcsc.com/api/products/search` with data 8 | `current_page=1&in_stock=false&is_RoHS=false&show_icon=false&search_content=` 9 | You have to include CSRF token and cookies. Both you can get from the category 10 | page (e.g., `https://lcsc.com/products/Pre-ordered-Products_11171.html`) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Jan Mrázek 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](web/public/favicon.svg) 2 | 3 | # JLC PCB SMD Assembly Component Catalogue 4 | 5 | A better tool to browse the components offered by the [JLC PCB SMT Assembly 6 | Service](https://jlcpcb.com/smt-assembly). 7 | 8 | ## How To Use It? 9 | 10 | Just visit: [https://yaqwsx.github.io/jlcparts/](https://yaqwsx.github.io/jlcparts/) 11 | 12 | ## Why? 13 | 14 | Probably all of us love JLC PCB SMT assembly service. It is easy to use, cheap 15 | and fast. However, you can use only components from [their 16 | catalogue](https://jlcpcb.com/parts). This is not as bad, since the library is 17 | quite broad. However, the library UI sucks. You can only browse the categories, 18 | do full-text search. You cannot do parametric search nor sort by property. 19 | That's why I created a simple page which presents the catalogue in much nicer 20 | form. You can: 21 | - do full-text search 22 | - browse categories 23 | - parametric search 24 | - sort by any component attribute 25 | - sort by price based on quantity 26 | - easily access datasheet and LCSC product page. 27 | 28 | ## Do You Enjoy It? Does It Make Your Life Easier? 29 | 30 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/E1E2181LU) 31 | 32 | Support on Ko-Fi allows me to develop such tools as this one and perform 33 | hardware-related experiments. 34 | 35 | ## How Does It Look Like? 36 | 37 | Title page 38 | 39 | ![Preview 1](https://user-images.githubusercontent.com/1590880/93708766-32ab0d80-fb39-11ea-8365-da2ca1b13d8b.jpg) 40 | 41 | Property filter 42 | 43 | ![Preview 2](https://user-images.githubusercontent.com/1590880/93708599-e01d2180-fb37-11ea-96b6-5d5eb4e0f285.jpg) 44 | 45 | Component detail 46 | 47 | ![Preview 3](https://user-images.githubusercontent.com/1590880/93708601-e0b5b800-fb37-11ea-84ed-6ba73f07911d.jpg) 48 | 49 | 50 | ## How Does It Work? 51 | 52 | The page has no backend so it can be easily hosted on GitHub Pages. Therefore, 53 | Travis CI download XLS spreadsheet from the JLC PCB page, a Python script 54 | process it and it generates per-category JSON file with components. 55 | 56 | The frontend uses IndexedDB in the browser to store the component library and 57 | perform queries on it. Therefore, before the first use, you have to download the 58 | component library and it can take a while. Then, all the queries are performed 59 | locally. 60 | 61 | ## Development 62 | 63 | To get started with developing the frontend, you will need NodeJS & Python 3. 64 | 65 | Set up the Python portion of the program by running: 66 | 67 | ``` 68 | $ virtualenv venv 69 | $ source venv/bin/activate 70 | $ pip install -e . 71 | ``` 72 | 73 | Then to download the cached parts list and process it, run: 74 | 75 | ``` 76 | $ wget https://yaqwsx.github.io/jlcparts/data/cache.zip https://yaqwsx.github.io/jlcparts/data/cache.z0{1..8} 77 | $ 7z x cache.zip 78 | $ mkdir -p web/public/data/ 79 | $ jlcparts buildtables --jobs 0 --ignoreoldstock 30 cache.sqlite3 web/public/data 80 | ``` 81 | 82 | To launch the frontend web server, run: 83 | 84 | ``` 85 | $ cd web 86 | $ npm install 87 | $ npm start 88 | ``` 89 | 90 | ## The Page Is Broken! 91 | 92 | Feel free to open an issue on GitHub. 93 | 94 | ## You Might Also Be Interested 95 | 96 | - [KiKit](https://github.com/yaqwsx/KiKit): a tool for automatic panelization of 97 | KiCAD PCBs. It can also perform fully automatic export of manufacturing data 98 | for JLC PCB assembly - read [the 99 | documentation](https://github.com/yaqwsx/KiKit/blob/master/doc/fabrication/jlcpcb.md) 100 | or produce a solder-paste stencil for populating components missing at JLC PCB - read [the 101 | documentation](https://github.com/yaqwsx/KiKit/blob/master/doc/stencil.md). 102 | - [PcbDraw](https://github.com/yaqwsx/PcbDraw): a tool for making nice schematic 103 | drawings of your boards and population manuals. 104 | -------------------------------------------------------------------------------- /jlcparts/attributes.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import math 4 | 5 | # This module tries to parse LSCS attribute strings into structured data The 6 | # whole process is messy and there is no strong guarantee it will work in all 7 | # cases: there are lots of inconsistencies and typos in the attributes. So we 8 | # try to deliver best effort results 9 | 10 | def erase(string, what): 11 | """ 12 | Given a string and a list of string, removes all occurences of items from 13 | what in the string 14 | """ 15 | for x in what: 16 | string = string.replace(x, "") 17 | return string 18 | 19 | def stringAttribute(value, name="default"): 20 | return { 21 | "format": "${" + name +"}", 22 | "primary": name, 23 | "values": { 24 | name: [value, "string"] 25 | } 26 | } 27 | 28 | def readWithSiPrefix(value): 29 | """ 30 | Given a string in format (without the actual unit), 31 | read its value. E.g., 10k ~> 10000, 10m ~> 0.01 32 | """ 33 | value = value.strip() 34 | if value == "-" or value == "" or value == "null": 35 | return "NaN" 36 | unitPrexies = { 37 | "p": 1e-12, 38 | "n": 1e-9, 39 | "u": 1e-6, 40 | "U": 1e-6, 41 | "μ": 1e-6, 42 | "µ": 1e-6, 43 | "?": 1e-3, # There is a common typo instead of 'm' there is '?' - the keys are close on keyboard 44 | "m": 1e-3, 45 | "k": 1e3, 46 | "K": 1e3, 47 | "M": 1e6, 48 | "G": 1e9 49 | } 50 | if value[-1].isalpha() or value[-1] == "?": # Again, watch for the ? typo 51 | return float(value[:-1]) * unitPrexies[value[-1]] 52 | return float(value) 53 | 54 | def readResistance(value): 55 | """ 56 | Given a string, try to parse resistance and return it as Ohms (float) 57 | """ 58 | value = erase(value, ["Ω", "Ohms", "Ohm", "(Max)", "Max"]).strip() 59 | value = value.replace(" ", "") # Sometimes there are spaces after decimal place 60 | unitPrefixes = { 61 | "m": [1e-3, 1e-6], 62 | "K": [1e3, 1], 63 | "k": [1e3, 1], 64 | "M": [1e6, 1e3], 65 | "G": [1e9, 1e6] 66 | } 67 | for prefix, table in unitPrefixes.items(): 68 | if prefix in value: 69 | split = [float(x) if x != "" else 0 for x in value.split(prefix)] 70 | value = split[0] * table[0] + split[1] * table[1] 71 | break 72 | if value == "-" or value == "" or value == "null": 73 | value = "NaN" 74 | else: 75 | value = float(value) 76 | return value 77 | 78 | def readCurrent(value): 79 | """ 80 | Given a string, try to parse current and return it as Amperes (float) 81 | """ 82 | value = erase(value, ["PNP"]) 83 | value = value.replace("A", "").strip() 84 | value = value.split("..")[-1] # Some transistors give a range for current in Rds 85 | if value in ["-", "--"] or "null" in value: 86 | return "NaN" 87 | v = readWithSiPrefix(value) 88 | return v 89 | 90 | def readVoltage(value): 91 | value = value.replace("v", "V") 92 | value = value.replace("V-", "V") 93 | value = value.replace("V", "").strip() 94 | if value in ["-", "--"] or "null" in value: 95 | return "NaN" 96 | return readWithSiPrefix(value) 97 | 98 | def readPower(value): 99 | """ 100 | Parse power value (in watts), it can also handle fractions 101 | """ 102 | if value in ["-", "--"] or "null" in value: 103 | return "NaN" 104 | if ";" in value: 105 | return readPower(value.split(";")[0]) 106 | if "/" in value: 107 | # Fraction 108 | numerator, denominator, unit = re.fullmatch(r"(\d+)/(\d+)\s*(\w+)", value).groups() 109 | value = str(float(numerator) / float(denominator)) + unit 110 | value = value.replace("W", "").strip() 111 | return readWithSiPrefix(value) 112 | 113 | def readCapacitance(value): 114 | value = value.replace("F", "").strip() 115 | if value in ["-", "--"] or "null" in value: 116 | return "NaN" 117 | return readWithSiPrefix(value) 118 | 119 | def readCharge(value): 120 | value = value.replace("C", "").strip() 121 | return readWithSiPrefix(value) 122 | 123 | def readFrequency(value): 124 | value = erase(value, ["Hz", "HZ", "H"]).strip() 125 | return readWithSiPrefix(value) 126 | 127 | def readInductance(value): 128 | value = value.replace("H", "").strip() 129 | return readWithSiPrefix(value) 130 | 131 | def resistanceAttribute(value): 132 | if ";" in value: 133 | # This is a resistor array 134 | values = value.split(value) 135 | values = [readResistance(x.strip()) for x in values] 136 | values = { "resistance" + (str(i + 1) if i != 0 else ""): [x, "resistance"] for i, x in enumerate(values)} 137 | format = ", ".join(values.keys()) 138 | return { 139 | "format": format, 140 | "primary": "resistance", 141 | "values": values 142 | } 143 | else: 144 | value = readResistance(value) 145 | return { 146 | "format": "${resistance}", 147 | "primary": "resistance", 148 | "values": { 149 | "resistance": [value, "resistance"] 150 | } 151 | } 152 | 153 | def impedanceAttribute(value): 154 | value = readResistance(value) 155 | return { 156 | "format": "${impedance}", 157 | "primary": "impedance", 158 | "values": { 159 | "impedance": [value, "resistance"] 160 | } 161 | } 162 | 163 | 164 | def voltageAttribute(value): 165 | value = re.sub(r"\(.*?\)", "", value) 166 | # Remove multiple current values 167 | value = value.split("x")[-1] 168 | value = value.split("/")[-1] 169 | value = value.split(",")[-1] 170 | value = value.split("~")[-1] 171 | value = value.split("or")[-1] 172 | value = value.replace("VIN", "V").replace("Vin", "V") 173 | value = value.replace("VDC", "V").replace("VAC", "V") 174 | value = value.replace("Vdc", "V").replace("Vac", "V") 175 | value = value.replace("X1:", "") 176 | value = value.replace("A", "V") # Common typo 177 | value = erase(value, "±") 178 | value = re.sub(";.*", "", value) 179 | 180 | if value.strip() in ["-", "Tracking", "nV"]: 181 | value = "NaN" 182 | else: 183 | value = readVoltage(value) 184 | return { 185 | "format": "${voltage}", 186 | "primary": "voltage", 187 | "values": { 188 | "voltage": [value, "voltage"] 189 | } 190 | } 191 | 192 | def currentAttribute(value): 193 | if value.lower().strip() == "adjustable": 194 | return { 195 | "format": "${current}", 196 | "default": "current", 197 | "values": { 198 | "current": ["NaN", "current"] 199 | } 200 | } 201 | if ";" in value: 202 | values = value.split(value) 203 | values = [readCurrent(x.strip()) for x in values] 204 | values = { "current" + (str(i + 1) if i != 0 else ""): [x, "current"] for i, x in enumerate(values)} 205 | format = ", ".join(values.keys()) 206 | return { 207 | "format": format, 208 | "primary": "current", 209 | "values": values 210 | } 211 | else: 212 | value = erase(value, ["±", "Up to"]) 213 | value = re.sub(r"\(.*?\)", "", value) 214 | # Remove multiple current values 215 | value = value.split("x")[-1] 216 | value = value.split("/")[-1] 217 | value = value.split(",")[-1] 218 | value = value.split("~")[-1] 219 | value = value.split("or")[-1] 220 | # Replace V/A typo 221 | value = value.replace("V", "A") 222 | value = readCurrent(value) 223 | return { 224 | "format": "${current}", 225 | "primary": "current", 226 | "values": { 227 | "current": [value, "current"] 228 | } 229 | } 230 | 231 | def powerAttribute(value): 232 | value = re.sub(r"\(.*?\)", "", value) 233 | # Replace V/W typo 234 | value = value.replace("V", "W") 235 | # Strip random additional characters (e.g., C108632) 236 | value = value.replace("S", "") 237 | 238 | p = readPower(value) 239 | return { 240 | "format": "${power}", 241 | "default": "power", 242 | "values": { 243 | "power": [p, "power"] 244 | } 245 | } 246 | 247 | def countAttribute(value): 248 | if value == "-": 249 | return { 250 | "format": "${count}", 251 | "default": "count", 252 | "values": { 253 | "count": ["NaN", "count"] 254 | } 255 | } 256 | value = erase(value, [" - Dual"]) 257 | value = re.sub(r"\(.*?\)", "", value) 258 | # There are expressions like a+b, so let's sum them 259 | try: 260 | count = sum(map(int, value.split("+"))) 261 | except ValueError: 262 | # Sometimes, there are floats in number of pins... God, why? 263 | # See, e.g., C2836126 264 | try: 265 | count = sum(map(float, value.split("+"))) 266 | except ValueError: 267 | # And sometimes there is garbage... 268 | count = "NaN" 269 | return { 270 | "format": "${count}", 271 | "default": "count", 272 | "values": { 273 | "count": [count, "count"] 274 | } 275 | } 276 | 277 | 278 | def capacitanceAttribute(value): 279 | # There are a handful of components, that feature multiple capacitance 280 | # values, for the sake of the simplicity, take the last one. 281 | value = readCapacitance(value.split(";")[-1].strip()) 282 | return { 283 | "format": "${capacitance}", 284 | "primary": "capacitance", 285 | "values": { 286 | "capacitance": [value, "capacitance"] 287 | } 288 | } 289 | 290 | def inductanceAttribute(value): 291 | value = readInductance(value) 292 | return { 293 | "format": "${inductance}", 294 | "primary": "inductance", 295 | "values": { 296 | "inductance": [value, "inductance"] 297 | } 298 | } 299 | 300 | def frequencyAttribute(value): 301 | value = readFrequency(value) 302 | return { 303 | "format": "${frequency}", 304 | "primary": "frequency", 305 | "values": { 306 | "frequency": [value, "frequency"] 307 | } 308 | } 309 | 310 | 311 | def rdsOnMaxAtIdsAtVgs(value): 312 | """ 313 | Given a string in format " @ , " parse it and 314 | return it as structured value 315 | """ 316 | def readRds(v): 317 | if v == "-": 318 | return "NaN", "NaN", "NaN" 319 | matched = re.fullmatch(r"([\w.]*)\s*[@\s]\s*([-\w.]*)\s*[,,]\s*([-~\w.]*)").groups() 320 | # There are some transistors with a typo; using "A" instead of "V" or Ω, fix it: 321 | resistance = matched.group(1).replace("A", "Ω") 322 | voltage = matched.group(3).replace("A", "V") 323 | if "~" in voltage: 324 | voltage = voltage.split("~")[-1] 325 | return (readResistance(resistance), 326 | readCurrent(matched.group(2)), 327 | readVoltage(voltage)) 328 | if value.count(",") == 3 or ";" in value: 329 | # Double P & N MOSFET 330 | if ";" in value: 331 | s = value.split(";") 332 | else: 333 | s = value.split(",") 334 | s = [s[0] + "," + s[1], s[2] + "," + s[3]] 335 | rds1, id1, vgs1 = readRds(s[0]) 336 | rds2, id2, vgs2 = readRds(s[1]) 337 | return { 338 | "format": "${Rds 1} @ ${Id 1}, ${Vgs 1}; ${Rds 2} @ ${Id 2}, ${Vgs 2}", 339 | "primary": "Rds 1", 340 | "values": { 341 | "Rds 2": [rds2, "resistance"], 342 | "Id 2": [id2, "current"], 343 | "Vgs 2": [vgs2, "voltage"], 344 | "Rds 1": [rds1, "resistance"], 345 | "Id 1": [id1, "current"], 346 | "Vgs 1": [vgs1, "voltage"] 347 | } 348 | } 349 | else: 350 | rds, ids, vgs = readRds(value) 351 | return { 352 | "format": "${Rds} @ ${Id}, ${Vgs}", 353 | "primary": "Rds", 354 | "values": { 355 | "Rds": [rds, "resistance"], 356 | "Id": [ids, "current"], 357 | "Vgs": [vgs, "voltage"] 358 | } 359 | } 360 | 361 | def rdsOnMaxAtVgsAtIds(value): 362 | """ 363 | Given a string in format " @ , " parse it and 364 | return it as structured value 365 | """ 366 | def readRds(v): 367 | if v == "-": 368 | return "NaN", "NaN", "NaN" 369 | # 370 | match = re.fullmatch( 371 | r"\s*([\w.]+)\s*(?:[@\s]\s*([-~\w.]+?)\s*(?:(?:[,,]|(?<=[vam])(?=\d))([-\w.]+)\s*)?)?", 372 | v, 373 | re.I 374 | ) 375 | if match is not None: 376 | resistance, voltage, current = match.groups() 377 | else: 378 | # There some components in the form 2.5Ω@VGS=10V, try this format 379 | resistance, voltage = re.fullmatch( 380 | r"\s*(.*Ω)\s*@\s*VGS=\s*(.*V)\s*", 381 | v, 382 | re.I 383 | ).groups() 384 | current = None 385 | 386 | if current is None: 387 | current = "-" 388 | if voltage is None: 389 | voltage = "-" 390 | 391 | if not current.endswith("A"): 392 | if current.endswith("V"): 393 | if voltage.endswith("A") or voltage.endswith("m"): 394 | # There are sometimes swapped values 395 | current, voltage = voltage, current 396 | else: 397 | current = current.replace("V", "A") 398 | else: 399 | current += "A" 400 | if voltage.endswith("A"): 401 | voltage = voltage.replace("A", "V") 402 | if "~" in voltage: 403 | voltage = voltage.split("~")[-1] 404 | return (readResistance(resistance), 405 | readCurrent(current), 406 | readVoltage(voltage)) 407 | if value.count(",") == 3 or ";" in value: 408 | # Double P & N MOSFET 409 | if ";" in value: 410 | s = value.split(";") 411 | else: 412 | s = value.split(",") 413 | s = [s[0] + "," + s[1], s[2] + "," + s[3]] 414 | rds1, id1, vgs1 = readRds(s[0]) 415 | rds2, id2, vgs2 = readRds(s[1]) 416 | return { 417 | "format": "${Rds 1} @ ${Vgs 1}, ${Id 1}; ${Rds 2} @ ${Vgs 2}, ${Id 2}", 418 | "primary": "Rds 1", 419 | "values": { 420 | "Rds 2": [rds2, "resistance"], 421 | "Id 2": [id2, "current"], 422 | "Vgs 2": [vgs2, "voltage"], 423 | "Rds 1": [rds1, "resistance"], 424 | "Id 1": [id1, "current"], 425 | "Vgs 1": [vgs1, "voltage"] 426 | } 427 | } 428 | else: 429 | rds, ids, vgs = readRds(value) 430 | return { 431 | "format": "${Rds} @ ${Vgs}, ${Id}", 432 | "primary": "Rds", 433 | "values": { 434 | "Rds": [rds, "resistance"], 435 | "Id": [ids, "current"], 436 | "Vgs": [vgs, "voltage"] 437 | } 438 | } 439 | 440 | 441 | def continuousTransistorCurrent(value, symbol): 442 | """ 443 | Can parse values like '10A', '10A,12A', '1OA(Tc)' 444 | """ 445 | value = re.sub(r"\(.*?\)", "", value) # Remove all notes about temperature 446 | value = erase(value, ["±"]) 447 | value = value.replace("V", "A") # There are some typos - voltage instead of current 448 | value = value.replace(";", ",") # Sometimes semicolon is used instead of comma 449 | if "," in value: 450 | # Double P & N MOSFET 451 | s = value.split(",") 452 | i1 = readCurrent(s[0]) 453 | i2 = readCurrent(s[1]) 454 | return { 455 | "format": "${" + symbol + " 1}, ${" + symbol + " 2}", 456 | "default": symbol + " 1", 457 | "values": { 458 | symbol + " 1": [i1, "current"], 459 | symbol + " 2": [i2, "current"] 460 | } 461 | } 462 | else: 463 | i = readCurrent(value) 464 | return { 465 | "format": "${" + symbol + "}", 466 | "default": symbol, 467 | "values": { 468 | symbol: [i, "current"] 469 | } 470 | } 471 | 472 | def drainToSourceVoltage(value): 473 | """ 474 | Can parse single or double voltage values" 475 | """ 476 | value = value.replace("A", "V") # There are some typos - current instead of voltage 477 | if "," in value: 478 | s = value.split(",") 479 | v1 = readVoltage(s[0]) 480 | v2 = readVoltage(s[1]) 481 | return { 482 | "format": "${Vds 1}, ${Vds 2}", 483 | "default": "Vds 1", 484 | "values": { 485 | "Vds 1": [v1, "voltage"], 486 | "Vds 2": [v1, "voltage"] 487 | } 488 | } 489 | else: 490 | v = readVoltage(value) 491 | return { 492 | "format": "${Vds}", 493 | "default": "Vds", 494 | "values": { 495 | "Vds": [v, "voltage"] 496 | } 497 | } 498 | 499 | def powerDissipation(value): 500 | """ 501 | Parse single or double power dissipation into structured value 502 | """ 503 | value = re.sub(r"\(.*?\)", "", value) # Remove all notes about temperature 504 | value = value.replace("V", "W") # Common typo 505 | if "A" in value: 506 | # The value is a clear nonsense 507 | return { 508 | "format": "${power}", 509 | "default": "power", 510 | "values": { 511 | "power": ["NaN", "power"] 512 | } 513 | } 514 | value = value.split("/")[-1] # When there are multiple thermal ratings for 515 | # transistors, choose the last as it is the most interesting one 516 | if "," in value: 517 | s = value.split(",") 518 | p1 = readPower(s[0]) 519 | p2 = readPower(s[1]) 520 | return { 521 | "format": "${power 1}, ${power 2}", 522 | "default": "power 1", 523 | "values": { 524 | "power 1": [p1, "power"], 525 | "power 2": [p2, "power"] 526 | } 527 | } 528 | else: 529 | p = readPower(value) 530 | return { 531 | "format": "${power}", 532 | "default": "power", 533 | "values": { 534 | "power": [p, "power"] 535 | } 536 | } 537 | 538 | def vgsThreshold(value): 539 | """ 540 | Parse single or double value in format ' @ ' 541 | """ 542 | def readVgs(v): 543 | if value == "-": 544 | return "NaN", "NaN" 545 | voltage, current = re.match(r"([-\w.]*)(?:[@| ]([-\w.]*))?", v).groups() 546 | if current is None: 547 | current = "-" 548 | return readVoltage(voltage), readCurrent(current) 549 | 550 | value = re.sub(r"\(.*?\)", "", value) 551 | if "," in value or ";" in value: 552 | splitchar = "," if "," in value else ";" 553 | s = value.split(splitchar) 554 | v1, i1 = readVgs(s[0]) 555 | v2, i2 = readVgs(s[1]) 556 | return { 557 | "format": "${Vgs 1} @ ${Id 1}, ${Vgs 2} @ ${Id 2}", 558 | "default": "Vgs 1", 559 | "values": { 560 | "Vgs 1": [v1, "voltage"], 561 | "Id 1": [i1, "current"], 562 | "Vgs 2": [v2, "voltage"], 563 | "Id 2": [i2, "current"] 564 | } 565 | } 566 | else: 567 | v, i = readVgs(value) 568 | return { 569 | "format": "${Vgs} @ ${Id}", 570 | "default": "Vgs", 571 | "values": { 572 | "Vgs": [v, "voltage"], 573 | "Id": [i, "current"] 574 | } 575 | } 576 | 577 | def esr(value): 578 | """ 579 | Parse equivalent series resistance in the form ' @ ' 580 | """ 581 | if value == "-": 582 | return { 583 | "format": "-", 584 | "default": "esr", 585 | "values": { 586 | "esr": ["NaN", "resistance"], 587 | "frequency": ["NaN", "frequency"] 588 | } 589 | } 590 | value = erase(value, ["(", ")"]) # For resonators, the value is enclosed in parenthesis 591 | matches = re.fullmatch(r"([\w.]*)\s*(?:[@\s]\s*([~\w.]*))?[.,]?", value) 592 | res = readResistance(matches.group(1)) 593 | if matches.group(2): 594 | freq = readFrequency(matches.group(2).split('~')[-1]) 595 | return { 596 | "format": "${esr} @ ${frequency}", 597 | "default": "esr", 598 | "values": { 599 | "esr": [res, "resistance"], 600 | "frequency": [freq, "frequency"] 601 | } 602 | } 603 | else: 604 | return { 605 | "format": "${esr}", 606 | "default": "esr", 607 | "values": { 608 | "esr": [res, "resistance"] 609 | } 610 | } 611 | 612 | def rippleCurrent(value): 613 | if value == "-": 614 | return { 615 | "format": "-", 616 | "default": "current", 617 | "values": { 618 | "current": ["NaN", "current"], 619 | "frequency": ["NaN", "frequency"] 620 | } 621 | } 622 | if value.endswith("-"): # Work around for trailing trash 623 | value = value[:-1] 624 | s = value.split("@") 625 | if len(s) == 1: 626 | s = value.split(" ") 627 | i = readCurrent(s[0]) 628 | if len(s) > 1: 629 | f = readFrequency(s[1].split("~")[-1]) 630 | else: 631 | f = "NaN" 632 | return { 633 | "format": "${current} @ ${frequency}", 634 | "default": "current", 635 | "values": { 636 | "current": [i, "current"], 637 | "frequency": [f, "frequency"] 638 | } 639 | } 640 | 641 | def sizeMm(value): 642 | if value == "-": 643 | return { 644 | "format": "-", 645 | "default": "width", 646 | "values": { 647 | "width": ["NaN", "length"], 648 | "height": ["NaN", "length"] 649 | } 650 | } 651 | value = value.lower() 652 | s = value.split("x") 653 | w = float(s[0]) / 1000 654 | h = float(s[1]) / 1000 655 | return { 656 | "format": "${width}×${height}", 657 | "default": "width", 658 | "values": { 659 | "width": [w, "length"], 660 | "height": [h, "length"] 661 | } 662 | } 663 | 664 | def forwardVoltage(value): 665 | if value == "-": 666 | return { 667 | "format": "-", 668 | "default": "Vf", 669 | "values": { 670 | "Vf": ["NaN", "voltage"], 671 | "If": ["NaN", "current"] 672 | } 673 | } 674 | value = erase(value, ["<"]) 675 | value = re.sub(r"\(.*?\)", "", value) 676 | s = value.split("@") 677 | 678 | vStr = s[0].replace("A", "V") # Common typo 679 | v = readVoltage(vStr) 680 | i = readCurrent(s[1]) 681 | return { 682 | "format": "${Vf} @ ${If}", 683 | "default": "Vf", 684 | "values": { 685 | "Vf": [v, "voltage"], 686 | "If": [i, "current"] 687 | } 688 | } 689 | 690 | def removeColor(string): 691 | """ 692 | If there is a color name in the string, remove it 693 | """ 694 | return erase(string, ["Red", "Green", "Blue", "Orange", "Yellow"]) 695 | 696 | def voltageRange(value): 697 | if value == "-": 698 | return { 699 | "format": "-", 700 | "default": "Vmin", 701 | "values": { 702 | "Vmin": ["NaN", "voltage"], 703 | "Vmax": ["NaN", "voltage"] 704 | } 705 | } 706 | value = re.sub(r"\(.*?\)", "", value) 707 | value = value.replace("A", "V") # Common typo 708 | value = value.split(",")[0].split(";")[0] # In the case of multivalue range 709 | if ".." in value: 710 | s = value.split("..") 711 | elif "-" in value: 712 | s = value.split("-") 713 | else: 714 | s = value.split("~") 715 | s = [removeColor(x) for x in s] # Something there is the color in the attributes 716 | vMin = s[0].split(",")[0].split("/")[0] 717 | vMin = readVoltage(vMin) 718 | if len(s) == 2: 719 | return { 720 | "format": "${Vmin} ~ ${Vmax}", 721 | "default": "Vmin", 722 | "values": { 723 | "Vmin": [vMin, "voltage"], 724 | "Vmax": [readVoltage(s[1]), "voltage"] 725 | } 726 | } 727 | return { 728 | "format": "${Vmin}", 729 | "default": "Vmin", 730 | "values": { 731 | "Vmin": [vMin, "voltage"] 732 | } 733 | } 734 | 735 | def clampingVoltage(value): 736 | if value == "-": 737 | return { 738 | "format": "-", 739 | "default": "Vc", 740 | "values": { 741 | "Vc": ["NaN", "voltage"], 742 | "Ic": ["NaN", "current"] 743 | } 744 | } 745 | value = re.sub(r"\(.*?\)", "", value) 746 | s = value.split("@") 747 | vC = s[0].split(",")[0].split("/")[0].split(";")[0] 748 | vC = vC.replace("A", "V") # Common typo 749 | vC = readVoltage(vC) 750 | if len(s) == 2: 751 | c = s[1].replace("V", "A") # Common typo 752 | return { 753 | "format": "${Vc} @ ${Ic}", 754 | "default": "Vc", 755 | "values": { 756 | "Vc": [vC, "voltage"], 757 | "Ic": [readCurrent(c), "current"] 758 | } 759 | } 760 | return { 761 | "format": "${Vc}", 762 | "default": "Vc", 763 | "values": { 764 | "Vc": [vC, "voltage"] 765 | } 766 | } 767 | 768 | def vceBreakdown(value): 769 | value = erase(value, "PNP").split(",")[0] 770 | return voltageAttribute(value) 771 | 772 | def vceOnMax(value): 773 | matched = re.match(r"(.*)@(.*),(.*)", value) 774 | if matched: 775 | vce = readVoltage(matched.group(1)) 776 | vge = readVoltage(matched.group(2)) 777 | ic = readCurrent(matched.group(3)) 778 | else: 779 | vce = "NaN" 780 | vge = "NaN" 781 | ic = "NaN" 782 | return { 783 | "format": "${Vce} @ ${Vge}, ${Ic}", 784 | "default": "Vce", 785 | "values": { 786 | "Vce": [vce, "voltage"], 787 | "Vge": [vge, "voltage"], 788 | "Ic": [ic, "current"] 789 | } 790 | } 791 | 792 | def temperatureAttribute(value): 793 | if value == "-": 794 | return { 795 | "format": "-", 796 | "default": "temperature", 797 | "values": { 798 | "temperature": ["NaN", "temperature"] 799 | } 800 | } 801 | value = erase(value, ["@"]) 802 | value = re.sub(r"\(.*?\)", "", value) 803 | value = value.strip() 804 | assert value.endswith("℃") 805 | value = erase(value, ["℃"]) 806 | v = int(value) 807 | return { 808 | "format": "${temperature}", 809 | "default": "temperature", 810 | "values": { 811 | "temperature": [v, "temperature"] 812 | } 813 | } 814 | 815 | def capacityAtVoltage(value): 816 | """ 817 | Parses @ 818 | """ 819 | if value == "-": 820 | return { 821 | "format": "-", 822 | "default": "capacity", 823 | "values": { 824 | "capacity": ["NaN", "capacitance"], 825 | "voltage": ["NaN", "voltage"] 826 | } 827 | } 828 | def readTheTuple(value): 829 | try: 830 | c, v = tuple(value.split("@")) 831 | except: 832 | try: 833 | c, v = tuple(value.split(" ")) 834 | except: 835 | # Sometimes, we miss voltage 836 | c = value 837 | v = None 838 | c = readCapacitance(c.strip()) 839 | if v is not None: 840 | v = readVoltage(v.strip()) 841 | else: 842 | v = "NaN" 843 | return c, v 844 | if ";" in value: 845 | a, b = tuple(value.split(";")) 846 | c1, v1 = readTheTuple(a) 847 | c2, v2 = readTheTuple(b) 848 | return { 849 | "format": "${capacity 1} @ ${voltage 1}; {capacity 2} @ ${voltage 2}", 850 | "default": "capacity 1", 851 | "values": { 852 | "capacity 1": [c1, "capacitance"], 853 | "voltage 1": [v1, "voltage"], 854 | "capacity 2": [c2, "capacitance"], 855 | "voltage 2": [v2, "voltage"] 856 | } 857 | } 858 | c, v = readTheTuple(value) 859 | return { 860 | "format": "${capacity} @ ${voltage}", 861 | "default": "capacity", 862 | "values": { 863 | "capacity": [c, "capacitance"], 864 | "voltage": [v, "voltage"] 865 | } 866 | } 867 | 868 | def chargeAtVoltage(value): 869 | """ 870 | Parses @ 871 | """ 872 | if value == "-": 873 | return { 874 | "format": "-", 875 | "default": "charge", 876 | "values": { 877 | "charge": ["NaN", "capacitance"], 878 | "voltage": ["NaN", "voltage"] 879 | } 880 | } 881 | def readTheTuple(value): 882 | match = re.match(r"(?P.*?)(\s*[ @](?P.*))?", value.strip()) 883 | if match is None: 884 | raise RuntimeError(f"Cannot parse charge at voltage for {value}") 885 | q = match.groupdict().get("cap") 886 | v = match.groupdict().get("voltage") 887 | 888 | if q is not None: 889 | q = readCharge(q.strip()) 890 | else: 891 | q = "NaN" 892 | 893 | if v is not None: 894 | v = readVoltage(re.sub(r'-?\d+~', '', v.strip())) 895 | else: 896 | v = "NaN" 897 | return q, v 898 | 899 | if ";" in value: 900 | a, b = tuple(value.split(";")) 901 | q1, v1 = readTheTuple(a) 902 | q2, v2 = readTheTuple(b) 903 | return { 904 | "format": "${charge 1} @ ${voltage 1}; ${charge 2} @ ${voltage 2}", 905 | "default": "charge 1", 906 | "values": { 907 | "charge 1": [q1, "charge"], 908 | "voltage 1": [v2, "voltage"], 909 | "charge 2": [q2, "charge"], 910 | "voltage 2": [v2, "voltage"] 911 | } 912 | } 913 | 914 | q, v = readTheTuple(value) 915 | return { 916 | "format": "${charge} @ ${voltage}", 917 | "default": "charge", 918 | "values": { 919 | "charge": [q, "charge"], 920 | "voltage": [v, "voltage"] 921 | } 922 | } 923 | -------------------------------------------------------------------------------- /jlcparts/common.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | def sha256file(filename): 4 | sha256_hash = hashlib.sha256() 5 | with open(filename, "rb") as f: 6 | for byte_block in iter(lambda: f.read(4096), b""): 7 | sha256_hash.update(byte_block) 8 | return sha256_hash.hexdigest() 9 | -------------------------------------------------------------------------------- /jlcparts/datatables.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import re 3 | import os 4 | import shutil 5 | import json 6 | import datetime 7 | import gzip 8 | import multiprocessing 9 | from pathlib import Path 10 | 11 | import click 12 | from jlcparts.partLib import PartLibraryDb 13 | from jlcparts.common import sha256file 14 | from jlcparts import attributes, descriptionAttributes 15 | 16 | def saveJson(object, filename, hash=False, pretty=False, compress=False): 17 | openFn = gzip.open if compress else open 18 | with openFn(filename, "wt", encoding="utf-8") as f: 19 | if pretty: 20 | json.dump(object, f, indent=4, sort_keys=True) 21 | else: 22 | json.dump(object, f, separators=(',', ':'), sort_keys=True) 23 | if hash: 24 | with open(filename + ".sha256", "w") as f: 25 | hash = sha256file(filename) 26 | f.write(hash) 27 | return hash 28 | 29 | def weakUpdateParameters(attrs, newParameters): 30 | for attr, value in newParameters.items(): 31 | if attr in attrs and attrs[attr] not in ["", "-"]: 32 | continue 33 | attrs[attr] = value 34 | 35 | def extractAttributesFromDescription(description): 36 | if description.startswith("Chip Resistor - Surface Mount"): 37 | return descriptionAttributes.chipResistor(description) 38 | if (description.startswith("Multilayer Ceramic Capacitors MLCC") or 39 | description.startswith("Aluminum Electrolytic Capacitors")): 40 | return descriptionAttributes.capacitor(description) 41 | return {} 42 | 43 | def normalizeUnicode(value): 44 | """ 45 | Replace unexpected unicode sequence with a resonable ones 46 | """ 47 | value = value.replace("(", " (").replace(")", ")") 48 | value = value.replace(",", ",") 49 | return value 50 | 51 | def normalizeAttribute(key, value): 52 | """ 53 | Takes a name of attribute and its value (usually a string) and returns a 54 | normalized attribute name and its value as a tuple. Normalized value is a 55 | dictionary in the format: 56 | { 57 | "format": , 59 | "values": 60 | } 61 | The fallback is unit "string" 62 | """ 63 | larr = lambda arr : map(lambda str : str.lower(), arr) 64 | normkey = normalizeAttributeKey(key) 65 | key = normkey.lower() 66 | if isinstance(value, str): 67 | value = normalizeUnicode(value) 68 | 69 | try: 70 | if key in larr(["Resistance", "Resistance in Ohms @ 25°C", "DC Resistance"]): 71 | value = attributes.resistanceAttribute(value) 72 | elif key in larr(["Balance Port Impedence", "Unbalance Port Impedence"]): 73 | value = attributes.impedanceAttribute(value) 74 | elif key in larr(["Voltage - Rated", "Voltage Rating - DC", "Allowable Voltage", 75 | "Clamping Voltage", "Varistor Voltage(Max)", "Varistor Voltage(Typ)", 76 | "Varistor Voltage(Min)", "Voltage - DC Reverse (Vr) (Max)", 77 | "Voltage - DC Spark Over (Nom)", "Voltage - Peak Reverse (Max)", 78 | "Voltage - Reverse Standoff (Typ)", "Voltage - Gate Trigger (Vgt) (Max)", 79 | "Voltage - Off State (Max)", "Voltage - Input (Max)", "Voltage - Output (Max)", 80 | "Voltage - Output (Fixed)", "Voltage - Output (Min/Fixed)", 81 | "Supply Voltage (Max)", "Supply Voltage (Min)", "Output Voltage", 82 | "Voltage - Input (Min)", "Drain Source Voltage (Vdss)"]): 83 | value = attributes.voltageAttribute(value) 84 | elif key in larr(["Rated current", "surge current", "Current - Average Rectified (Io)", 85 | "Current - Breakover", "Current - Peak Output", "Current - Peak Pulse (10/1000μs)", 86 | "Impulse Discharge Current (8/20us)", "Current - Gate Trigger (Igt) (Max)", 87 | "Current - On State (It (AV)) (Max)", "Current - On State (It (RMS)) (Max)", 88 | "Current - Supply (Max)", "Output Current", "Output Current (Max)", 89 | "Output / Channel Current", "Current - Output", 90 | "Saturation Current (Isat)"]): 91 | value = attributes.currentAttribute(value) 92 | elif key in larr(["Power", "Power Per Element", "Power Dissipation (Pd)"]): 93 | value = attributes.powerAttribute(value) 94 | elif key in larr(["Number of Pins", "Number of Resistors", "Number of Loop", 95 | "Number of Regulators", "Number of Outputs", "Number of Capacitors"]): 96 | value = attributes.countAttribute(value) 97 | elif key in larr(["Capacitance"]): 98 | value = attributes.capacitanceAttribute(value) 99 | elif key in larr(["Inductance"]): 100 | value = attributes.inductanceAttribute(value) 101 | elif key == "Rds On (Max) @ Id, Vgs".lower(): 102 | value = attributes.rdsOnMaxAtIdsAtVgs(value) 103 | elif key in larr(["Operating Temperature (Max)", "Operating Temperature (Min)"]): 104 | value = attributes.temperatureAttribute(value) 105 | elif key.startswith("Continuous Drain Current"): 106 | value = attributes.continuousTransistorCurrent(value, "Id") 107 | elif key == "Current - Collector (Ic) (Max)".lower(): 108 | value = attributes.continuousTransistorCurrent(value, "Ic") 109 | elif key in larr(["Vgs(th) (Max) @ Id", "Gate Threshold Voltage (Vgs(th)@Id)"]): 110 | value = attributes.vgsThreshold(value) 111 | elif key.startswith("Drain to Source Voltage"): 112 | value = attributes.drainToSourceVoltage(value) 113 | elif key == "Drain Source On Resistance (RDS(on)@Vgs,Id)".lower(): 114 | value = attributes.rdsOnMaxAtVgsAtIds(value) 115 | elif key == "Power Dissipation-Max (Ta=25°C)".lower(): 116 | value = attributes.powerDissipation(value) 117 | elif key in larr(["Equivalent Series Resistance", "Impedance @ Frequency"]): 118 | value = attributes.esr(value) 119 | elif key == "Ripple Current".lower(): 120 | value = attributes.rippleCurrent(value) 121 | elif key == "Size(mm)".lower(): 122 | value = attributes.sizeMm(value) 123 | elif key == "Voltage - Forward (Vf) (Max) @ If".lower(): 124 | value = attributes.forwardVoltage(value) 125 | elif key in larr(["Voltage - Breakdown (Min)", "Voltage - Zener (Nom) (Vz)", 126 | "Vf - Forward Voltage"]): 127 | value = attributes.voltageRange(value) 128 | elif key == "Voltage - Clamping (Max) @ Ipp".lower(): 129 | value = attributes.clampingVoltage(value) 130 | elif key == "Voltage - Collector Emitter Breakdown (Max)".lower(): 131 | value = attributes.vceBreakdown(value) 132 | elif key == "Vce(on) (Max) @ Vge, Ic".lower(): 133 | value = attributes.vceOnMax(value) 134 | elif key in larr(["Input Capacitance (Ciss@Vds)", 135 | "Reverse Transfer Capacitance (Crss@Vds)"]): 136 | value = attributes.capacityAtVoltage(value) 137 | elif key in larr(["Total Gate Charge (Qg@Vgs)"]): 138 | value = attributes.chargeAtVoltage(value) 139 | elif key in larr(["Frequency - self resonant", "Output frequency (max)"]): 140 | value = attributes.frequencyAttribute(value) 141 | else: 142 | value = attributes.stringAttribute(value) 143 | except: 144 | print(f"Could not process key {normkey}; obj {value}") 145 | value = attributes.stringAttribute(value) # fall back to string -- these values should have their patterns updated 146 | 147 | assert isinstance(value, dict) 148 | return normkey, value 149 | 150 | def normalizeCapitalization(key): 151 | """ 152 | Given a category name, normalize capitalization. We turn everything 153 | lowercase, but some known substring (such as MOQ or MHz) replace back to the 154 | correct capitalization 155 | """ 156 | key = key.lower() 157 | CAPITALIZATIONS = [ 158 | "Basic/Extended", "MHz", "GHz", "Hz", "MOQ" 159 | ] 160 | for capt in CAPITALIZATIONS: 161 | key = key.replace(capt.lower(), capt) 162 | key = key[0].upper() + key[1:] 163 | return key 164 | 165 | def normalizeAttributeKey(key): 166 | """ 167 | Takes a name of attribute and its value and returns a normalized key 168 | (e.g., strip unit name). 169 | """ 170 | if "(Watts)" in key: 171 | key = key.replace("(Watts)", "").strip() 172 | if "(Ohms)" in key: 173 | key = key.replace("(Ohms)", "").strip() 174 | if key == "aristor Voltage(Min)": 175 | key = "Varistor Voltage(Min)" 176 | if key in ["ESR (Equivalent Series Resistance)", "Equivalent Series Resistance(ESR)"] or key.startswith("Equivalent Series Resistance"): 177 | key = "Equivalent Series Resistance" 178 | if key in ["Allowable Voltage(Vdc)", "Voltage - Max", "Rated Voltage"] or key.startswith("Voltage Rated"): 179 | key = "Allowable Voltage" 180 | if key in ["DC Resistance (DCR)", "DC Resistance (DCR) (Max)", "DCR( Ω Max )"]: 181 | key = "DC Resistance" 182 | if key in ["Insertion Loss ( dB Max )", "Insertion Loss (Max)"]: 183 | key = "Insertion Loss (dB Max)" 184 | if key in ["Current Rating (Max)", "Rated Current"]: 185 | key = "Rated current" 186 | if key == "Power - Max": 187 | key = "Power" 188 | if key == "Voltage - Breakover": 189 | key = "Voltage - Breakdown (Min)" 190 | if key == "Gate Threshold Voltage-VGE(th)": 191 | key = "Vgs(th) (Max) @ Id" 192 | if key == "Pins Structure": 193 | key = "Pin Structure" 194 | if key.startswith("Lifetime @ Temp"): 195 | key = "Lifetime @ Temperature" 196 | if key.startswith("Q @ Freq"): 197 | key = "Q @ Frequency" 198 | return normalizeCapitalization(key) 199 | 200 | def pullExtraAttributes(component): 201 | """ 202 | Turn common properties (e.g., base/extended) into attributes. Return them as 203 | a dictionary 204 | """ 205 | status = "Discontinued" if component["extra"] == {} else "Active" 206 | type = "Extended" 207 | if component["basic"]: 208 | type = "Basic" 209 | if component["preferred"]: 210 | type = "Preferred" 211 | return { 212 | "Basic/Extended": type, 213 | "Package": component["package"], 214 | "Status": status 215 | } 216 | 217 | def crushImages(images): 218 | if not images: 219 | return None 220 | firstImg = images[0] 221 | img = firstImg.popitem()[1].rsplit("/", 1)[1] 222 | # make sure every url ends the same 223 | assert all(i.rsplit("/", 1)[1] == img for i in firstImg.values()) 224 | return img 225 | 226 | def trimLcscUrl(url, lcsc): 227 | if url is None: 228 | return None 229 | slug = url[url.rindex("/") + 1 : url.rindex("_")] 230 | assert f"https://lcsc.com/product-detail/{slug}_{lcsc}.html" == url 231 | return slug 232 | 233 | def extractComponent(component, schema): 234 | try: 235 | propertyList = [] 236 | for schItem in schema: 237 | if schItem == "attributes": 238 | # The cache might be in the old format 239 | if "attributes" in component.get("extra", {}): 240 | attr = component.get("extra", {}).get("attributes", {}) 241 | else: 242 | attr = component.get("extra", {}) 243 | if isinstance(attr, list): 244 | # LCSC return empty attributes as a list, not dictionary 245 | attr = {} 246 | attr.update(pullExtraAttributes(component)) 247 | weakUpdateParameters(attr, extractAttributesFromDescription(component["description"])) 248 | 249 | # Remove extra attributes that are either not useful, misleading 250 | # or overridden by data from JLC 251 | attr.pop("url", None) 252 | attr.pop("images", None) 253 | attr.pop("prices", None) 254 | attr.pop("datasheet", None) 255 | attr.pop("id", None) 256 | attr.pop("manufacturer", None) 257 | attr.pop("number", None) 258 | attr.pop("title", None) 259 | attr.pop("quantity", None) 260 | for i in range(10): 261 | attr.pop(f"quantity{i}", None) 262 | 263 | attr["Manufacturer"] = component.get("manufacturer", None) 264 | 265 | attr = dict([normalizeAttribute(key, val) for key, val in attr.items()]) 266 | propertyList.append(attr) 267 | elif schItem == "img": 268 | images = component.get("extra", {}).get("images", None) 269 | propertyList.append(crushImages(images)) 270 | elif schItem == "url": 271 | url = component.get("extra", {}).get("url", None) 272 | propertyList.append(trimLcscUrl(url, component["lcsc"])) 273 | elif schItem in component: 274 | item = component[schItem] 275 | if isinstance(item, str): 276 | item = item.strip() 277 | propertyList.append(item) 278 | else: 279 | propertyList.append(None) 280 | return propertyList 281 | except Exception as e: 282 | raise RuntimeError(f"Cannot extract {component['lcsc']}").with_traceback(e.__traceback__) 283 | 284 | def buildDatatable(components): 285 | schema = ["lcsc", "mfr", "joints", "description", 286 | "datasheet", "price", "img", "url", "attributes"] 287 | return { 288 | "schema": schema, 289 | "components": [extractComponent(x, schema) for x in components] 290 | } 291 | 292 | def buildStocktable(components): 293 | return {component["lcsc"]: component["stock"] for component in components } 294 | 295 | def clearDir(directory): 296 | """ 297 | Delete everything inside a directory 298 | """ 299 | for filename in os.listdir(directory): 300 | file_path = os.path.join(directory, filename) 301 | if os.path.isfile(file_path) or os.path.islink(file_path): 302 | os.unlink(file_path) 303 | elif os.path.isdir(file_path): 304 | shutil.rmtree(file_path) 305 | 306 | 307 | @dataclasses.dataclass 308 | class MapCategoryParams: 309 | libraryPath: str 310 | outdir: str 311 | ignoreoldstock: int 312 | 313 | catName: str 314 | subcatName: str 315 | 316 | 317 | def _map_category(val: MapCategoryParams): 318 | # Sometimes, JLC PCB doesn't fill in the category names. Ignore such 319 | # components. 320 | if val.catName.strip() == "": 321 | return None 322 | if val.subcatName.strip() == "": 323 | return None 324 | 325 | lib = PartLibraryDb(val.libraryPath) 326 | components = lib.getCategoryComponents(val.catName, val.subcatName, stockNewerThan=val.ignoreoldstock) 327 | if not components: 328 | return None 329 | 330 | filebase = val.catName + val.subcatName 331 | filebase = filebase.replace("&", "and").replace("/", "aka") 332 | filebase = re.sub('[^A-Za-z0-9]', '_', filebase) 333 | 334 | dataTable = buildDatatable(components) 335 | dataTable.update({"category": val.catName, "subcategory": val.subcatName}) 336 | dataHash = saveJson(dataTable, os.path.join(val.outdir, f"{filebase}.json.gz"), 337 | hash=True, compress=True) 338 | 339 | stockTable = buildStocktable(components) 340 | stockHash = saveJson(stockTable, os.path.join(val.outdir, f"{filebase}.stock.json"), hash=True) 341 | 342 | return { 343 | "catName": val.catName, 344 | "subcatName": val.subcatName, 345 | "sourcename": filebase, 346 | "datahash": dataHash, 347 | "stockhash": stockHash 348 | } 349 | 350 | @click.command() 351 | @click.argument("library", type=click.Path(dir_okay=False)) 352 | @click.argument("outdir", type=click.Path(file_okay=False)) 353 | @click.option("--ignoreoldstock", type=int, default=None, 354 | help="Ignore components that weren't on stock for more than n days") 355 | @click.option("--jobs", type=int, default=1, 356 | help="Number of parallel processes. Defaults to 1, set to 0 to use all cores") 357 | def buildtables(library, outdir, ignoreoldstock, jobs): 358 | """ 359 | Build datatables out of the LIBRARY and save them in OUTDIR 360 | """ 361 | lib = PartLibraryDb(library) 362 | Path(outdir).mkdir(parents=True, exist_ok=True) 363 | clearDir(outdir) 364 | 365 | 366 | total = lib.countCategories() 367 | categoryIndex = {} 368 | 369 | params = [] 370 | for (catName, subcategories) in lib.categories().items(): 371 | for subcatName in subcategories: 372 | params.append(MapCategoryParams( 373 | libraryPath=library, outdir=outdir, ignoreoldstock=ignoreoldstock, 374 | catName=catName, subcatName=subcatName)) 375 | 376 | with multiprocessing.Pool(jobs or multiprocessing.cpu_count()) as pool: 377 | for i, result in enumerate(pool.imap_unordered(_map_category, params)): 378 | if result is None: 379 | continue 380 | catName, subcatName = result["catName"], result["subcatName"] 381 | print(f"{((i) / total * 100):.2f} % {catName}: {subcatName}") 382 | if catName not in categoryIndex: 383 | categoryIndex[catName] = {} 384 | assert subcatName not in categoryIndex[catName] 385 | categoryIndex[catName][subcatName] = { 386 | "sourcename": result["sourcename"], 387 | "datahash": result["datahash"], 388 | "stockhash": result["stockhash"] 389 | } 390 | index = { 391 | "categories": categoryIndex, 392 | "created": datetime.datetime.now().astimezone().replace(microsecond=0).isoformat() 393 | } 394 | saveJson(index, os.path.join(outdir, "index.json"), hash=True) 395 | -------------------------------------------------------------------------------- /jlcparts/descriptionAttributes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def chipResistor(description): 4 | attrs = {} 5 | 6 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?Ohms", description) 7 | if matches is not None: 8 | attrs["Resistance"] = matches.group(0) 9 | 10 | matches = re.search(r"±\d+(\.\d+)?%", description) 11 | if matches is not None: 12 | attrs["Tolerance"] = matches.group(0) 13 | 14 | matches = re.search(r"((\d+/\d+)|(\d+(.\d+)?[a-zA-Z]?))W", description) 15 | if matches is not None: 16 | attrs["Power"] = matches.group(0) 17 | 18 | return attrs 19 | 20 | def capacitor(description): 21 | attrs = {} 22 | 23 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?F", description) 24 | if matches is not None: 25 | attrs["Capacitance"] = matches.group(0) 26 | 27 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?V", description) 28 | if matches is not None: 29 | attrs["Voltage - Rated"] = matches.group(0) 30 | 31 | return attrs 32 | -------------------------------------------------------------------------------- /jlcparts/jlcpcb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import csv 4 | import time 5 | from typing import Optional, List, Any, Callable 6 | 7 | JLCPCB_KEY = os.environ.get("JLCPCB_KEY") 8 | JLCPCB_SECRET = os.environ.get("JLCPCB_SECRET") 9 | 10 | class JlcPcbInterface: 11 | def __init__(self, key: str, secret: str) -> None: 12 | self.key = key 13 | self.secret = secret 14 | self.token = None 15 | self.lastPage = None 16 | 17 | def _obtainToken(self) -> None: 18 | body = { 19 | "appKey": self.key, 20 | "appSecret": self.secret 21 | } 22 | headers = { 23 | "Content-Type": "application/json", 24 | } 25 | resp = requests.post("https://jlcpcb.com/external/genToken", 26 | json=body, headers=headers) 27 | if resp.status_code != 200: 28 | raise RuntimeError(f"Cannot obtain token {resp.json()}") 29 | data = resp.json() 30 | if data["code"] != 200: 31 | raise RuntimeError(f"Cannot obtain toke {data}") 32 | self.token = data["data"] 33 | 34 | def getPage(self) -> Optional[List[Any]]: 35 | if self.token is None: 36 | self._obtainToken() 37 | headers = { 38 | "externalApiToken": self.token, 39 | } 40 | if self.lastPage is None: 41 | body = {} 42 | else: 43 | body = { 44 | "lastKey": self.lastPage 45 | } 46 | resp = requests.post("https://jlcpcb.com/external/component/getComponentInfos", 47 | data=body, headers=headers) 48 | try: 49 | data = resp.json()["data"] 50 | except: 51 | raise RuntimeError(f"Cannot fetch page: {resp.text}") 52 | self.lastPage = data["lastKey"] 53 | return data["componentInfos"] 54 | 55 | def dummyReporter(progress) -> None: 56 | return 57 | 58 | def pullComponentTable(filename: str, reporter: Callable[[int], None] = dummyReporter, 59 | retries: int = 10, retryDelay: int = 5) -> None: 60 | interf = JlcPcbInterface(JLCPCB_KEY, JLCPCB_SECRET) 61 | with open(filename, "w", encoding="utf-8") as f: 62 | writer = csv.writer(f) 63 | writer.writerow([ 64 | "LCSC Part", 65 | "First Category", 66 | "Second Category", 67 | "MFR.Part", 68 | "Package", 69 | "Solder Joint", 70 | "Manufacturer", 71 | "Library Type", 72 | "Description", 73 | "Datasheet", 74 | "Stock", 75 | "Price" 76 | ]) 77 | count = 0 78 | while True: 79 | for i in range(retries): 80 | try: 81 | page = interf.getPage() 82 | break 83 | except Exception as e: 84 | if i == retries - 1: 85 | raise e from None 86 | time.sleep(retryDelay) 87 | if page is None: 88 | break 89 | for c in page: 90 | writer.writerow([ 91 | c["lcscPart"], 92 | c["firstCategory"], 93 | c["secondCategory"], 94 | c["mfrPart"], 95 | c["package"], 96 | c["solderJoint"], 97 | c["manufacturer"], 98 | c["libraryType"], 99 | c["description"], 100 | c["datasheet"], 101 | c["stock"], 102 | c["price"] 103 | ]) 104 | count += len(page) 105 | reporter(count) 106 | -------------------------------------------------------------------------------- /jlcparts/lcsc.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import os 4 | import time 5 | import random 6 | import string 7 | import urllib 8 | import hashlib 9 | from requests.exceptions import ConnectionError 10 | 11 | LCSC_KEY = os.environ.get("LCSC_KEY") 12 | LCSC_SECRET = os.environ.get("LCSC_SECRET") 13 | 14 | def makeLcscRequest(url, payload=None): 15 | if payload is None: 16 | payload = {} 17 | payload = [(key, value) for key, value in payload.items()] 18 | payload.sort(key=lambda x: x[0]) 19 | newPayload = { 20 | "key": LCSC_KEY, 21 | "nonce": "".join(random.choices(string.ascii_lowercase, k=16)), 22 | "secret": LCSC_SECRET, 23 | "timestamp": str(int(time.time())), 24 | } 25 | for k, v in payload: 26 | newPayload[k] = v 27 | payloadStr = urllib.parse.urlencode(newPayload).encode("utf-8") 28 | newPayload["signature"] = hashlib.sha1(payloadStr).hexdigest() 29 | 30 | return requests.get(url, params=newPayload) 31 | 32 | def pullPreferredComponents(): 33 | resp = requests.get("https://jlcpcb.com/api/overseas-pcb-order/v1/getAll") 34 | token = resp.cookies.get_dict()["XSRF-TOKEN"] 35 | 36 | headers = { 37 | "Content-Type": "application/json", 38 | "X-XSRF-TOKEN": token, 39 | } 40 | PAGE_SIZE = 1000 41 | 42 | currentPage = 1 43 | components = set() 44 | while True: 45 | body = { 46 | "currentPage": currentPage, 47 | "pageSize": PAGE_SIZE, 48 | "preferredComponentFlag": True 49 | } 50 | 51 | resp = requests.post( 52 | "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList", 53 | headers=headers, 54 | json=body 55 | ) 56 | 57 | body = resp.json() 58 | for c in [x["componentCode"] for x in body["data"]["componentPageInfo"]["list"]]: 59 | components.add(c) 60 | 61 | if not body["data"]["componentPageInfo"]["hasNextPage"]: 62 | break 63 | currentPage += 1 64 | 65 | return components 66 | 67 | if __name__ == "__main__": 68 | r = makeLcscRequest("https://ips.lcsc.com/rest/wmsc2agent/product/info/C7063") 69 | print(r.json()) 70 | 71 | -------------------------------------------------------------------------------- /jlcparts/migrate.py: -------------------------------------------------------------------------------- 1 | import click 2 | from .partLib import PartLibrary, PartLibraryDb 3 | 4 | 5 | @click.command() 6 | @click.argument("input") 7 | @click.argument("output") 8 | def migrate_to_db(input, output): 9 | pLib = PartLibrary(input) 10 | dbLib = PartLibraryDb(output) 11 | 12 | with dbLib.startTransaction(): 13 | l = len(pLib.index) 14 | for i, id in enumerate(pLib.index.keys()): 15 | c = pLib.getComponent(id) 16 | if i % 1000 == 0: 17 | print(f"{((i+1) / l * 100):.2f} %") 18 | dbLib.addComponent(c) 19 | 20 | 21 | if __name__ == "__main__": 22 | migrate_to_db() 23 | -------------------------------------------------------------------------------- /jlcparts/partLib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import csv 4 | import json 5 | import os 6 | import sqlite3 7 | import time 8 | import urllib.parse 9 | from contextlib import contextmanager 10 | from pathlib import Path 11 | from textwrap import indent 12 | 13 | from .lcsc import makeLcscRequest 14 | 15 | if os.environ.get("JLCPARTS_DEV", "0") == "1": 16 | print("Using caching from /tmp/jlcparts") 17 | CACHE_PATH = Path("/tmp/jlcparts") 18 | CACHE_PATH.mkdir(parents=True, exist_ok=True) 19 | else: 20 | CACHE_PATH = None 21 | 22 | def normalizeCategoryName(catname): 23 | return catname 24 | # If you want to normalize category names, don't; you will break LCSC links! 25 | # return catname.replace("?", "") 26 | 27 | def lcscToDb(val): 28 | return int(val[1:]) 29 | 30 | def lcscFromDb(val): 31 | return f"C{val}" 32 | 33 | def dbToComp(comp): 34 | comp = dict(comp) 35 | comp["lcsc"] = lcscFromDb(comp["lcsc"]) 36 | comp["price"] = json.loads(comp["price"]) 37 | comp["extra"] = json.loads(comp["extra"]) 38 | comp["basic"] = bool(comp["basic"]) 39 | comp["preferred"] = bool(comp["preferred"]) 40 | return comp 41 | 42 | class PartLibraryDb: 43 | def __init__(self, filepath=None): 44 | self.conn = sqlite3.connect(filepath) 45 | self.conn.row_factory = sqlite3.Row 46 | self.transation = False 47 | self.categoryCache = {} 48 | self.manufacturerCache = {} 49 | 50 | self.conn.execute(""" 51 | CREATE TABLE IF NOT EXISTS components ( 52 | lcsc INTEGER PRIMARY KEY NOT NULL, 53 | category_id INTEGER NOT NULL, 54 | mfr TEXT NOT NULL, 55 | package TEXT NOT NULL, 56 | joints INTEGER NOT NULL, 57 | manufacturer_id INTEGER NOT NULL, 58 | basic INTEGER NOT NULL, 59 | preferred INTEGER NOT NULL DEFAULT 0, 60 | description TEXT NOT NULL, 61 | datasheet TEXT NOT NULL, 62 | stock INTEGER NOT NULL, 63 | price TEXT NOT NULL, 64 | last_update INTEGER NOT NULL, 65 | extra TEXT, 66 | flag INTEGER NOT NULL DEFAULT 0 67 | )""") 68 | 69 | # Perform migration if we miss last on stock 70 | migrated = False 71 | columns = list(self.conn.execute("pragma table_info(components)")) 72 | if "last_on_stock" not in [x[1] for x in columns]: 73 | self.conn.execute(""" 74 | ALTER TABLE components ADD COLUMN last_on_stock INTEGER NOT NULL DEFAULT 0; 75 | """) 76 | migrated = True 77 | 78 | # Perform migration if we miss the preferred flag 79 | if "preferred" not in [x[1] for x in columns]: 80 | self.conn.execute(""" 81 | ALTER TABLE components ADD COLUMN preferred INTEGER NOT NULL DEFAULT 0; 82 | """) 83 | migrated = True 84 | 85 | if migrated: 86 | self.conn.execute("DROP VIEW v_components") 87 | 88 | self.conn.execute(""" 89 | CREATE INDEX IF NOT EXISTS components_category 90 | ON components (category_id) 91 | """) 92 | self.conn.execute(""" 93 | CREATE INDEX IF NOT EXISTS components_manufacturer 94 | ON components (manufacturer_id) 95 | """) 96 | self.conn.execute(""" 97 | CREATE TABLE IF NOT EXISTS manufacturers ( 98 | id INTEGER PRIMARY KEY NOT NULL, 99 | name TEXT NOT NULL, 100 | UNIQUE (id, name) 101 | )""") 102 | self.conn.execute(""" 103 | CREATE TABLE IF NOT EXISTS categories ( 104 | id INTEGER PRIMARY KEY NOT NULL, 105 | category TEXT NOT NULL, 106 | subcategory TEXT NOT NULL, 107 | UNIQUE (id, category, subcategory) 108 | )""") 109 | self.conn.execute(""" 110 | CREATE VIEW IF NOT EXISTS v_components AS 111 | SELECT 112 | c.lcsc AS lcsc, 113 | c.category_id AS category_id, 114 | cat.category AS category, 115 | cat.subcategory AS subcategory, 116 | c.mfr AS mfr, 117 | c.package AS package, 118 | c.joints AS joints, 119 | m.name AS manufacturer, 120 | c.basic AS basic, 121 | c.preferred as preferred, 122 | c.description AS description, 123 | c.datasheet AS datasheet, 124 | c.stock AS stock, 125 | c.last_on_stock as last_on_stock, 126 | c.price AS price, 127 | c.extra AS extra 128 | FROM components c 129 | LEFT JOIN manufacturers m ON c.manufacturer_id = m.id 130 | LEFT JOIN categories cat ON c.category_id = cat.id 131 | """) 132 | self.conn.commit() 133 | 134 | def _commit(self): 135 | """ 136 | Commits automatically if no transaction is opened 137 | """ 138 | if not self.transation: 139 | self.conn.commit() 140 | 141 | def vacuum(self): 142 | self.conn.execute("VACUUM") 143 | 144 | def resetFlag(self, value=0): 145 | self.conn.execute("UPDATE components SET flag = ?", (value,)) 146 | self._commit() 147 | 148 | def countFlag(self, value=0): 149 | return self.conn.execute("SELECT COUNT() FROM components WHERE flag = ?", 150 | (value,)).fetchone()[0] 151 | 152 | def countCategories(self,): 153 | return self.conn.execute("SELECT COUNT() FROM categories").fetchone()[0] 154 | 155 | def removeWithFlag(self, value=1): 156 | self.conn.execute("DELETE FROM components WHERE flag = ?", (value,)) 157 | self._commit() 158 | 159 | @contextmanager 160 | def startTransaction(self): 161 | assert self.transation == False 162 | try: 163 | with self.conn: 164 | self.transation = True 165 | yield self 166 | finally: 167 | self.transation = False 168 | 169 | def close(self): 170 | self.conn.close() 171 | 172 | def getComponent(self, lcscNumber): 173 | result = self.conn.execute(""" 174 | SELECT * FROM v_components 175 | WHERE lcsc = ? 176 | LIMIT 1 177 | """, (lcscToDb(lcscNumber),)).fetchone() 178 | return dbToComp(result) 179 | 180 | def exists(self, lcscNumber): 181 | result = self.conn.execute(""" 182 | SELECT lcsc FROM components 183 | WHERE lcsc = ? 184 | LIMIT 1 185 | """, (lcscToDb(lcscNumber),)).fetchone() 186 | return result is not None 187 | 188 | def getCategoryId(self, category, subcategory): 189 | c = (category, subcategory) 190 | catId = self.manufacturerCache.get(c, None) 191 | if catId is not None: 192 | return catId 193 | catId = self.conn.execute(""" 194 | SELECT id FROM categories WHERE category = ? AND subcategory = ? 195 | """, c).fetchone() 196 | if catId is not None: 197 | catId = catId[0] 198 | return catId 199 | 200 | def getOrCreateCategoryId(self, category, subcategory): 201 | catId = self.getCategoryId(category, subcategory) 202 | if catId is not None: 203 | return catId 204 | c = (category, subcategory) 205 | cur = self.conn.cursor() 206 | cur.execute(""" 207 | INSERT INTO categories (category, subcategory) VALUES (?, ?) 208 | """, c) 209 | catId = cur.lastrowid 210 | self._commit() 211 | self.categoryCache[c] = catId 212 | return catId 213 | 214 | def getCategoryComponents(self, category, subcategory, stockNewerThan=None): 215 | """ 216 | Return an iterable of category components that have been in stock in the 217 | last stockNewerThan 218 | """ 219 | catId = self.getCategoryId(category, subcategory) 220 | if stockNewerThan is None: 221 | result = self.conn.cursor().execute(""" 222 | SELECT * FROM v_components WHERE category_id = ? 223 | """, (catId,)) 224 | else: 225 | result = self.conn.cursor().execute(""" 226 | SELECT * FROM v_components WHERE category_id = ? and last_on_stock > ? 227 | """, (catId, int(time.time()) - stockNewerThan * 24 * 3600)) 228 | return list(map(dbToComp, result)) 229 | 230 | def addComponent(self, component, flag=None): 231 | cur = self.conn.cursor() 232 | m = component["manufacturer"] 233 | manId = self.manufacturerCache.get(m, None) 234 | if manId is None: 235 | manId = cur.execute(""" 236 | SELECT id FROM manufacturers WHERE name = ? 237 | """, (m,)).fetchone() 238 | if manId is not None: 239 | manId = manId[0] 240 | if manId is None: 241 | cur.execute(""" 242 | INSERT INTO manufacturers (name) VALUES (?) 243 | """,(m,)) 244 | manId = cur.lastrowid 245 | self.manufacturerCache[m] = manId 246 | 247 | catId = self.getOrCreateCategoryId(component["category"], component["subcategory"]) 248 | 249 | c = component 250 | data = [lcscToDb(c["lcsc"]), catId, c["mfr"], c["package"], c["joints"], manId, 251 | c["basic"], c["description"], c["datasheet"], c["stock"], 252 | json.dumps(c["price"]), int(time.time()), json.dumps(c["extra"])] 253 | if flag is not None: 254 | data.append(flag) 255 | cur.execute(f""" 256 | INSERT INTO components 257 | (lcsc, category_id, mfr, package, joints, manufacturer_id, 258 | basic, description, datasheet, stock, price, last_update, 259 | extra {', flag' if flag is not None else ''}) 260 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? {', ?' if flag is not None else ''}) 261 | """, data) 262 | self._commit() 263 | 264 | def updateExtra(self, lcsc, extra): 265 | self.conn.execute(f""" 266 | UPDATE components 267 | SET extra = ?, last_update = ? 268 | WHERE lcsc = ? 269 | """, (json.dumps(extra), int(time.time()), lcscToDb(lcsc))) 270 | self._commit() 271 | 272 | def updateJlcPart(self, component, flag=None): 273 | """ 274 | Return if the update was successful or not 275 | """ 276 | c = component 277 | stock = int(c["stock"]) 278 | 279 | data = [c["mfr"], 280 | c["package"], 281 | c["joints"], 282 | c["basic"], 283 | c["description"], 284 | c["datasheet"], 285 | stock, 286 | json.dumps(c["price"])] 287 | if flag is not None: 288 | data.append(flag) 289 | if stock != 0: 290 | data.append(int(time.time())) 291 | data.append(lcscToDb(c["lcsc"])) 292 | 293 | cursor = self.conn.cursor() 294 | res = cursor.execute(f""" 295 | UPDATE components 296 | SET mfr = ?, 297 | package = ?, 298 | joints = ?, 299 | basic = ?, 300 | description = ?, 301 | datasheet = ?, 302 | stock = ?, 303 | price = ? 304 | {', flag = ?' if flag is not None else ''} 305 | {', last_on_stock = ?' if stock != 0 else ''} 306 | WHERE lcsc = ? 307 | """, data) 308 | self._commit() 309 | 310 | def setPreferred(self, lcscSet): 311 | cursor = self.conn.cursor() 312 | cursor.execute("UPDATE components SET preferred = 0") 313 | cursor.execute(f"UPDATE components SET preferred = 1 WHERE lcsc IN ({','.join(len(lcscSet) * ['?'])})", 314 | [lcscToDb(x) for x in lcscSet]) 315 | self._commit() 316 | 317 | def categories(self): 318 | res = {} 319 | for x in self.conn.cursor().execute("SELECT id, category, subcategory FROM categories"): 320 | category = x["category"] 321 | subcat = x["subcategory"] 322 | if category in res: 323 | res[category].append(subcat) 324 | else: 325 | res[category] = [subcat] 326 | self.categoryCache[(category, subcat)] = x["id"] 327 | return res 328 | 329 | def delete(self, lcscNumber): 330 | self.conn.execute("DELETE FROM components WHERE lcsc = ?", (lcscToDb(lcscNumber),)) 331 | self._commit() 332 | 333 | def getNOldest(self, count): 334 | cursor = self.conn.cursor() 335 | result = cursor.execute("SELECT lcsc FROM components ORDER BY last_update ASC LIMIT ?", (count,)) 336 | return map(lambda x: lcscFromDb(x["lcsc"]), result) 337 | 338 | 339 | class PartLibrary: 340 | def __init__(self, filepath=None): 341 | if filepath is None: 342 | self.lib = {} 343 | self.index = {} 344 | return 345 | with open(filepath, "r") as f: 346 | self.lib = json.load(f) 347 | 348 | # Normalize category names 349 | for _, category in self.lib.items(): 350 | keys = list(category.keys()) 351 | for k in keys: 352 | category[normalizeCategoryName(k)] = category.pop(k) 353 | keys = list(self.lib.keys()) 354 | for k in keys: 355 | self.lib[normalizeCategoryName(k)] = self.lib.pop(k) 356 | 357 | self.buildIndex() 358 | self.checkLibraryStructure() 359 | 360 | def buildIndex(self): 361 | index = {} 362 | for catName, category in self.lib.items(): 363 | for subCatName, subcategory in category.items(): 364 | for component in subcategory.keys(): 365 | if component in index: 366 | raise RuntimeError(f"Component {component} is in multiple categories") 367 | index[component] = (catName, subCatName) 368 | self.index = index 369 | 370 | def checkLibraryStructure(self): 371 | # ToDo 372 | pass 373 | 374 | def getComponent(self, lcscNumber): 375 | if lcscNumber not in self.index: 376 | return None 377 | cat, subcat = self.index[lcscNumber] 378 | return self.lib[cat][subcat][lcscNumber] 379 | 380 | def exists(self, lcscNumber): 381 | return lcscNumber in self.index 382 | 383 | def addComponent(self, component): 384 | cat = component["category"] 385 | subcat = component["subcategory"] 386 | if cat not in self.lib: 387 | self.lib[cat] = {} 388 | if subcat not in self.lib[cat]: 389 | self.lib[cat][subcat] = {} 390 | self.lib[cat][subcat][component["lcsc"]] = component 391 | self.index[component["lcsc"]] = (cat, subcat) 392 | 393 | def categories(self): 394 | """ 395 | Return a dict with list of available categories in form category -> 396 | [subcategory] 397 | """ 398 | return { category: subcategories.keys() for category, subcategories in self.lib.items()} 399 | 400 | def delete(self, lcscNumber): 401 | cat, subcat = self.index[lcscNumber] 402 | del self.lib[cat][subcat][lcscNumber] 403 | del self.index[lcscNumber] 404 | 405 | def deleteNOldest(self, count): 406 | if count == 0: 407 | return set() 408 | components = [self.getComponent(x) for x in self.index.keys()] 409 | components.sort(key=lambda x: x["extraTimestamp"] if "extraTimestamp" in x else 0) 410 | deleted = [] 411 | for i in range(count): 412 | deleted.append(components[i]["lcsc"]) 413 | self.delete(components[i]["lcsc"]) 414 | return set(deleted) 415 | 416 | def save(self, filename): 417 | with open(filename, "w") as f: 418 | json.dump(self.lib, f) 419 | 420 | def loadPartLibrary(file): 421 | lib = json.load(file) 422 | checkLibraryStructure(lib) 423 | return lib 424 | 425 | def parsePrice(priceString): 426 | prices = [] 427 | if len(priceString.strip()) == 0: 428 | return [] 429 | for price in priceString.split(","): 430 | if len(price) == 0: 431 | continue 432 | range, p = tuple(price.split(":")) 433 | qFrom, qTo = range.split("-") 434 | prices.append({ 435 | "qFrom": int(qFrom), 436 | "qTo": int(qTo) if qTo else None, 437 | "price": float(p) 438 | }) 439 | prices.sort(key=lambda x: x["qFrom"]) 440 | return prices 441 | 442 | 443 | def normalizeUrlPart(part): 444 | return (part 445 | .replace("(", "") 446 | .replace(")", "") 447 | .replace(" ", "-") 448 | .replace("/", "-") 449 | ) 450 | 451 | class FetchError(RuntimeError): 452 | def __init__(self, message, reason=None): 453 | super().__init__(message) 454 | self.reason = reason 455 | 456 | def getLcscExtraNew(lcscNumber, retries=10): 457 | timeouts = [ 458 | "502 Bad Gateway", 459 | "504 Gateway Time-out", 460 | "504 ERROR", 461 | "Too Many Requests", 462 | "Please try again in a few minutes", 463 | "403 Forbidden" 464 | ] 465 | 466 | try: 467 | if retries == 0: 468 | raise FetchError("Too many retries", None) 469 | # Try to load fetched data from cache - useful when developing (saves time 470 | # to fetch) 471 | try: 472 | if CACHE_PATH is None: 473 | raise RuntimeError("Cache not used") 474 | with open(CACHE_PATH / f"{lcscNumber}.json") as f: 475 | resJson = json.load(f) 476 | params = resJson["result"] 477 | except: 478 | # Not in cache, fetch 479 | res = None 480 | resJson = None 481 | try: 482 | res = makeLcscRequest(f"https://ips.lcsc.com/rest/wmsc2agent/product/info/{lcscNumber}") 483 | if res.status_code != 200: 484 | if any([x in res.text for x in timeouts]): 485 | raise TimeoutError(res.text) 486 | resJson = res.json() 487 | if resJson["code"] in [563, 564, 429]: 488 | # The component was not found on LCSC - probably discontinued 489 | return {} 490 | if resJson["code"] != 200: 491 | if resJson["code"] == 437: # Rate limit exceeded 492 | print(f"Rate limit exceeded for {lcscNumber}. Retrying in 1 minute... ({retries-1} retries left)") 493 | time.sleep(60) 494 | return getLcscExtraNew(lcscNumber, retries=retries-1) 495 | else: 496 | raise RuntimeError(f"{resJson['code']}: {resJson['message']}") 497 | params = resJson["result"] 498 | except TimeoutError as e: 499 | raise e from None 500 | except Exception as e: 501 | message = f"{res.status_code}: {res.text}" 502 | raise FetchError(message, e) from None 503 | # Save to cache, make development more pleasant 504 | if CACHE_PATH is not None: 505 | with open(CACHE_PATH / f"{lcscNumber}.json", "w") as f: 506 | json.dump(resJson, f) 507 | 508 | catalogName = urllib.parse.quote_plus(normalizeUrlPart(params["category"]["name2"])) 509 | man = urllib.parse.quote_plus(normalizeUrlPart(params["manufacturer"]["name"])) 510 | product = urllib.parse.quote_plus(normalizeUrlPart(params["title"])) 511 | code = urllib.parse.quote_plus(params["number"]) 512 | params["url"] = f"https://lcsc.com/product-detail/{catalogName}_{man}-{product}_{code}.html" 513 | 514 | return params 515 | except TimeoutError as e: 516 | time.sleep(60) 517 | return getLcscExtraNew(lcscNumber, retries=retries-1) 518 | except FetchError as e: 519 | reason = f"{e}: \n{e.reason}" 520 | print(f"Failed {lcscNumber}:\n" + indent(reason, 8 * " ")) 521 | raise e from None 522 | 523 | def loadJlcTable(file): 524 | reader = csv.DictReader(file, delimiter=',', quotechar='"') 525 | return { x["LCSC Part"]: { 526 | "lcsc": x["LCSC Part"], 527 | "category": x["First Category"], 528 | "subcategory": x["Second Category"], 529 | "mfr": x["MFR.Part"], 530 | "package": x["Package"], 531 | "joints": int(x["Solder Joint"]), 532 | "manufacturer": x["Manufacturer"], 533 | "basic": x["Library Type"].lower() == "base", 534 | "description": x["Description"], 535 | "datasheet": x["Datasheet"], 536 | "stock": int(x["Stock"]), 537 | "price": parsePrice(x["Price"]) 538 | } for x in reader } 539 | 540 | def loadJlcTableLazy(file): 541 | reader = csv.DictReader(file, delimiter=',', quotechar='"') 542 | return map( lambda x: { 543 | "lcsc": x["LCSC Part"], 544 | "category": x["First Category"], 545 | "subcategory": x["Second Category"], 546 | "mfr": x["MFR.Part"], 547 | "package": x["Package"], 548 | "joints": int(x["Solder Joint"]), 549 | "manufacturer": x["Manufacturer"], 550 | "basic": x["Library Type"].lower() == "base", 551 | "description": x["Description"], 552 | "datasheet": x["Datasheet"], 553 | "stock": int(x["Stock"]), 554 | "price": parsePrice(x["Price"]) 555 | }, reader ) 556 | -------------------------------------------------------------------------------- /jlcparts/ui.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | import json 3 | 4 | import click 5 | 6 | from jlcparts.datatables import buildtables, normalizeAttribute 7 | from jlcparts.lcsc import pullPreferredComponents 8 | from jlcparts.partLib import (PartLibrary, PartLibraryDb, getLcscExtraNew, 9 | loadJlcTable, loadJlcTableLazy) 10 | 11 | 12 | def fetchLcscData(lcsc): 13 | extra = getLcscExtraNew(lcsc) 14 | return (lcsc, extra) 15 | 16 | @click.command() 17 | @click.argument("source", type=click.Path(dir_okay=False, exists=True)) 18 | @click.argument("db", type=click.Path(dir_okay=False, writable=True)) 19 | @click.option("--age", type=int, default=0, 20 | help="Automatically discard n oldest components and fetch them again") 21 | @click.option("--limit", type=int, default=10000, 22 | help="Limit number of newly added components") 23 | def getLibrary(source, db, age, limit): 24 | """ 25 | Download library inside OUTPUT (JSON format) based on SOURCE (csv table 26 | provided by JLC PCB). 27 | 28 | You can specify previously downloaded library as a cache to save requests to 29 | fetch LCSC extra data. 30 | """ 31 | OLD = 0 32 | REFRESHED = 1 33 | 34 | db = PartLibraryDb(db) 35 | missing = set() 36 | total = 0 37 | with db.startTransaction(): 38 | db.resetFlag(value=OLD) 39 | with open(source, newline="") as f: 40 | jlcTable = loadJlcTableLazy(f) 41 | for component in jlcTable: 42 | total += 1 43 | if db.exists(component["lcsc"]): 44 | db.updateJlcPart(component, flag=REFRESHED) 45 | else: 46 | component["extra"] = {} 47 | db.addComponent(component, flag=REFRESHED) 48 | missing.add(component["lcsc"]) 49 | print(f"New {len(missing)} components out of {total} total") 50 | ageCount = min(age, max(0, limit - len(missing))) 51 | print(f"{ageCount} components will be aged and thus refreshed") 52 | missing = missing.union(db.getNOldest(ageCount)) 53 | 54 | # Truncate the missing components to respect the limit: 55 | missing = list(missing)[:limit] 56 | 57 | with Pool(processes=10) as pool: 58 | for i, (lcsc, extra) in enumerate(pool.imap_unordered(fetchLcscData, missing)): 59 | print(f" {lcsc} fetched. {((i+1) / len(missing) * 100):.2f} %") 60 | db.updateExtra(lcsc, extra) 61 | db.removeWithFlag(value=OLD) 62 | # Temporary work-around for space-related issues in CI - simply don't rebuild the DB 63 | # db.vacuum() 64 | 65 | 66 | 67 | @click.command() 68 | @click.argument("db", type=click.Path(dir_okay=False, writable=True)) 69 | def updatePreferred(db): 70 | """ 71 | Download list of preferred components from JLC PCB and mark them into the DB. 72 | """ 73 | preferred = pullPreferredComponents() 74 | lib = PartLibraryDb(db) 75 | lib.setPreferred(preferred) 76 | 77 | 78 | @click.command() 79 | @click.argument("libraryFilename") 80 | def listcategories(libraryfilename): 81 | """ 82 | Print all categories from library specified by LIBRARYFILENAMEto standard 83 | output 84 | """ 85 | lib = PartLibrary(libraryfilename) 86 | for c, subcats in lib.categories().items(): 87 | print(f"{c}:") 88 | for s in subcats: 89 | print(f" {s}") 90 | 91 | @click.command() 92 | @click.argument("libraryFilename") 93 | def listattributes(libraryfilename): 94 | """ 95 | Print all keys in the extra["attributes"] arguments from library specified by 96 | LIBRARYFILENAME to standard output 97 | """ 98 | keys = set() 99 | lib = PartLibrary(libraryfilename) 100 | for subcats in lib.lib.values(): 101 | for parts in subcats.values(): 102 | for data in parts.values(): 103 | if "extra" not in data: 104 | continue 105 | extra = data["extra"] 106 | attr = extra.get("attributes", {}) 107 | if not isinstance(attr, list): 108 | for k in extra.get("attributes", {}).keys(): 109 | keys.add(k) 110 | for k in keys: 111 | print(k) 112 | 113 | @click.command() 114 | @click.argument("lcsc_code") 115 | def fetchDetails(lcsc_code): 116 | """ 117 | Fetch LCSC extra information for a given LCSC code 118 | """ 119 | print(getLcscExtraNew(lcsc_code)) 120 | 121 | @click.command() 122 | @click.argument("filename", type=click.Path(writable=True)) 123 | @click.option("--verbose", is_flag=True, 124 | help="Be verbose") 125 | def fetchTable(filename, verbose): 126 | """ 127 | Fetch JLC PCB component table 128 | """ 129 | from .jlcpcb import pullComponentTable 130 | 131 | def report(count: int) -> None: 132 | if (verbose): 133 | print(f"Fetched {count}") 134 | 135 | pullComponentTable(filename, report) 136 | 137 | @click.command() 138 | @click.argument("lcsc") 139 | def testComponent(lcsc): 140 | """ 141 | Tests parsing attributes of given component 142 | """ 143 | extra = getLcscExtraNew(lcsc)["attributes"] 144 | 145 | extra.pop("url", None) 146 | extra.pop("images", None) 147 | extra.pop("prices", None) 148 | extra.pop("datasheet", None) 149 | extra.pop("id", None) 150 | extra.pop("manufacturer", None) 151 | extra.pop("number", None) 152 | extra.pop("title", None) 153 | extra.pop("quantity", None) 154 | for i in range(10): 155 | extra.pop(f"quantity{i}", None) 156 | normalized = dict(normalizeAttribute(key, val) for key, val in extra.items()) 157 | print(json.dumps(normalized, indent=4)) 158 | 159 | 160 | @click.group() 161 | def cli(): 162 | pass 163 | 164 | cli.add_command(getLibrary) 165 | cli.add_command(listcategories) 166 | cli.add_command(listattributes) 167 | cli.add_command(buildtables) 168 | cli.add_command(updatePreferred) 169 | cli.add_command(fetchDetails) 170 | cli.add_command(fetchTable) 171 | cli.add_command(testComponent) 172 | 173 | if __name__ == "__main__": 174 | cli() 175 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import setuptools 4 | 5 | 6 | with open("README.md", "r") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup( 10 | name="jlcparts", 11 | version="0.1.0", 12 | author="Jan Mrázek", 13 | author_email="email@honzamrazek.cz", 14 | description="Better view of JLC PCB parts", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/RoboticsBrno/JLCPCB-Parts", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | install_requires=[ 25 | "requests", 26 | "click", 27 | "lxml" 28 | ], 29 | setup_requires=[ 30 | 31 | ], 32 | zip_safe=False, 33 | include_package_data=True, 34 | entry_points = { 35 | "console_scripts": [ 36 | "jlcparts=jlcparts.ui:cli" 37 | ], 38 | } 39 | ) -------------------------------------------------------------------------------- /test/testParts.csv: -------------------------------------------------------------------------------- 1 | LCSC Part;First Category;Second Category;MFR.Part;Package;Solder Joint;Manufacturer;Library Type;Description;Datasheet;Price;Stock 2 | C25725;Resistors;Resistor Networks & Arrays;4D02WGJ0103TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 10KOhms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0103TCE_C25725.pdf;1-199:0.005210145,200-:0.001866667;67586 3 | C25726;Resistors;Resistor Networks & Arrays;4D02WGJ0102TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 1KOhms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0102TCE_C25726.pdf;1-199:0.005210145,200-:0.001866667;78875 4 | C25501;Resistors;Resistor Networks & Arrays;4D02WGJ0330TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 33Ohms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0330TCE_C25501.pdf;1-199:0.004749275,200-:0.001701449;76307 5 | C32375;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012T220KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012T220KTF_C32375.pdf;1-199:0.026200000,200-:0.012500000;43999 6 | C51725;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012Q3R3KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012Q3R3KTF_C51725.pdf;1-199:0.016815942,200-:0.007427536;27602 7 | C14304;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012T470KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012T470KTF_C14304.pdf;1-99:0.043747826,100-:0.023369565;26497 8 | C1042;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012Q1R0KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012Q1R0KTF_C1042.pdf;1-199:0.017931884,200-:0.008240580;37413 9 | C1043;Inductors & Chokes & Transformers;Inductors (SMD);CMI201209U2R2KT;0805;2;Guangdong Fenghua Advanced Tech;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-CMI201209U2R2KT_C1043.pdf;1-199:0.010449275,200-:0.004427536;34565 10 | C1046;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012S100KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012S100KTF_C1046.pdf;1-199:0.017739130,200-:0.008152174;96901 11 | C17902;Resistors;Chip Resistor - Surface Mount;1206W4F1002T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 10KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F1002T5E_C17902.pdf;1-199:0.005760870,200-:0.002156522;600403 12 | C17903;Resistors;Chip Resistor - Surface Mount;1206W4F100JT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 10Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F100JT5E_C17903.pdf;1-199:0.004056522,200-:0.001453623;180708 13 | C17924;Resistors;Chip Resistor - Surface Mount;1206W4F1800T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 180Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F1800T5E_C17924.pdf;1-199:0.007044928,200-:0.002752174;43294 14 | C17928;Resistors;Chip Resistor - Surface Mount;1206W4F100KT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 1Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F100KT5E_C17928.pdf;1-199:0.007086957,200-:0.002768116;50315 15 | C17936;Resistors;Chip Resistor - Surface Mount;1206W4F4701T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 4.7KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F4701T5E_C17936.pdf;1-199:0.005760870,200-:0.002156522;555574 16 | C17944;Resistors;Chip Resistor - Surface Mount;1206W4F2001T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 2KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F2001T5E_C17944.pdf;1-199:0.004091304,200-:0.001465217;141238 17 | C17955;Resistors;Chip Resistor - Surface Mount;1206W4F200JT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 20Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F200JT5E_C17955.pdf;1-199:0.005837681,200-:0.002185507;277566 18 | C1558;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG2R0C500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 2pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG2R0C500NT_C1558.pdf;1-999:0.001739130,1000-:0.000543478;63891 19 | C1561;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG2R7C500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 2.7pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG2R7C500NT_C1561.pdf;1-999:0.001739130,1000-:0.000543478;59863 20 | C1554;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG200J500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 20pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG200J500NT_C1554.pdf;1-199:0.005649275,200-:0.002114493;91205 21 | C1530;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402B221K500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 220pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402B221K500NT_C1530.pdf;1-199:0.010688406,200-:0.004528986;79360 22 | C1532;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402B223K500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 22nF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402B223K500NT_C1532.pdf;1-199:0.010688406,200-:0.004528986;110653 23 | C1557;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG270J500NTN;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 27pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG270J500NTN_C1557.pdf;1-199:0.012878261,200-:0.005688406;101996 24 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Generated css 26 | src/main.css 27 | 28 | # Data 29 | public/data 30 | -------------------------------------------------------------------------------- /web/.modernizrrc: -------------------------------------------------------------------------------- 1 | { 2 | "minify": true, 3 | "options": [ 4 | "setClasses" 5 | ], 6 | "feature-detects": [ 7 | "test/indexeddb" 8 | ] 9 | } -------------------------------------------------------------------------------- /web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jlcparts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@discoveryjs/natural-compare": "^1.0.0", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 8 | "@fortawesome/free-brands-svg-icons": "^5.14.0", 9 | "@fortawesome/free-regular-svg-icons": "^5.14.0", 10 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 11 | "@fortawesome/react-fontawesome": "^0.1.11", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.5.0", 14 | "@testing-library/user-event": "^7.2.1", 15 | "dexie": "^3.0.2", 16 | "immer": "^7.0.8", 17 | "pako": "^2.0.4", 18 | "react": "^16.13.1", 19 | "react-copy-to-clipboard": "^5.0.2", 20 | "react-dom": "^16.13.1", 21 | "react-lazy-load-image-component": "^1.5.0", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "^5.0.1", 24 | "react-scroll": "^1.8.1", 25 | "react-waypoint": "^9.0.3" 26 | }, 27 | "scripts": { 28 | "start": "npm run watch:css && react-scripts start", 29 | "build": "npm run build:css && react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject", 32 | "build:css": "postcss src/tailwind.css -o src/main.css", 33 | "watch:css": "postcss src/tailwind.css -o src/main.css" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "homepage": ".", 51 | "devDependencies": { 52 | "autoprefixer": "^9.8.6", 53 | "postcss-cli": "^9.1.0", 54 | "postcss-flexbugs-fixes": "^5.0.2", 55 | "postcss-normalize": "^10.0.1", 56 | "postcss-preset-env": "^7.7.1", 57 | "tailwindcss": "^1.8.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | tailwindcss('./tailwind.js'), 6 | require('autoprefixer') 7 | ], 8 | }; -------------------------------------------------------------------------------- /web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/brokenimage.svg: -------------------------------------------------------------------------------- 1 | Created by Ryan Beckfrom the Noun Project -------------------------------------------------------------------------------- /web/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 38 | JLC Component Catalogue 39 | 40 | 41 | 42 |
43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /web/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/mstile-144x144.png -------------------------------------------------------------------------------- /web/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/mstile-150x150.png -------------------------------------------------------------------------------- /web/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/mstile-310x150.png -------------------------------------------------------------------------------- /web/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/mstile-310x310.png -------------------------------------------------------------------------------- /web/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/5fa91e1562a69d0abdd0246d11d20f07c3cb28ca/web/public/mstile-70x70.png -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /web/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | HashRouter as Router, 4 | Switch, 5 | Route, 6 | NavLink 7 | } from "react-router-dom"; 8 | 9 | import { library } from '@fortawesome/fontawesome-svg-core' 10 | import { fas } from '@fortawesome/free-solid-svg-icons' 11 | import { far } from '@fortawesome/free-regular-svg-icons' 12 | import { fab } from '@fortawesome/free-brands-svg-icons' 13 | 14 | import './main.css'; 15 | import { updateComponentLibrary, checkForComponentLibraryUpdate, db } from './db' 16 | import { ComponentOverview } from './componentTable' 17 | import { History } from './history' 18 | 19 | 20 | library.add(fas, far, fab); 21 | 22 | function Header(props) { 23 | return <> 24 |
25 | 26 |
27 |

28 | JLC PCB SMD Assembly Component Catalogue 29 |

30 |

31 | Parametric search for components offered by JLC PCB SMD assembly service. 32 |

33 |

34 | Read more at project's GitHub page. 35 |

36 |
37 |
38 |
39 | Do you enjoy this site? Consider supporting me so I can actively maintain projects like this one! 40 | Read more about my story. 41 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 55 | 60 | 61 | 62 |
45 | GitHub Sponsors: 46 | 48 | 49 |
53 | Ko-Fi: 54 | 56 | 57 | Ko-Fi button 58 | 59 |
63 |
64 | 65 | } 66 | 67 | function Footer(props) { 68 | return
69 | 70 |
71 | } 72 | 73 | class FirstTimeNote extends React.Component { 74 | constructor(props) { 75 | super(props); 76 | this.state = { 77 | componentCount: undefined 78 | }; 79 | } 80 | 81 | componentDidMount() { 82 | db.components.count().then(x => { 83 | this.setState({componentCount: x}); 84 | }) 85 | } 86 | 87 | render() { 88 | if (this.state.componentCount === undefined || this.state.componentCount !== 0) 89 | return null; 90 | return
91 |

92 | Hey, it seems that you run the application for the first time, hence, 93 | there's no component library in your device. Just press the "Update 94 | the component library button" in the upper right corner to download it 95 | and use the app. 96 |

97 |

98 | Note that the initial download of the component library might take a while. 99 |

100 |
101 | } 102 | } 103 | 104 | class NewComponentFormatWarning extends React.Component { 105 | constructor(props) { 106 | super(props); 107 | this.state = { 108 | newComponentFormat: true 109 | }; 110 | } 111 | 112 | componentDidMount() { 113 | db.components.toCollection().first().then(x => { 114 | if (x !== undefined && typeof x.attributes[Object.keys(x.attributes)[0]] !== 'object') 115 | this.setState({newComponentFormat: false}); 116 | }); 117 | } 118 | 119 | render() { 120 | if (this.state.newComponentFormat) 121 | return null; 122 | return
123 |

124 | Hey, there have been some breaking changes to the library format. 125 | Please, update the library before continuing to use the tool. 126 |

127 |
128 | } 129 | } 130 | 131 | class UpdateBar extends React.Component { 132 | constructor(props) { 133 | super(props); 134 | this.state = { 135 | updateAvailable: this.props.updateAvailable 136 | }; 137 | } 138 | 139 | componentDidMount() { 140 | let checkStatus = () => { 141 | checkForComponentLibraryUpdate().then( updateAvailable => { 142 | this.setState({updateAvailable}); 143 | }); 144 | db.settings.get("lastUpdate").then(lastUpdate => { 145 | this.setState({lastUpdate}); 146 | }) 147 | }; 148 | 149 | checkStatus(); 150 | this.timerID = setInterval(checkStatus, 60000); 151 | } 152 | 153 | componentWillUnmount() { 154 | clearInterval(this.timerID); 155 | } 156 | 157 | handleUpdateClick = e => { 158 | e.preventDefault(); 159 | this.props.onTriggerUpdate(); 160 | } 161 | 162 | render() { 163 | if (this.state.updateAvailable) { 164 | return
165 |

There is an update of the component library available.

166 | 170 |
171 | } 172 | else { 173 | return
174 |

The component database is up to-date {this.state.lastUpdate ? `(${this.state.lastUpdate})` : ""}.

175 |
176 | } 177 | } 178 | } 179 | 180 | class Updater extends React.Component { 181 | constructor(props) { 182 | super(props); 183 | this.state = { 184 | progress: {} 185 | }; 186 | } 187 | 188 | componentDidMount() { 189 | let t0 = performance.now(); 190 | updateComponentLibrary( 191 | progress => { this.setState({progress}); } 192 | ).then(() => { 193 | let t1 = performance.now(); 194 | console.log("Library update took ", t1 - t0, "ms"); 195 | this.props.onFinish(); 196 | }); 197 | } 198 | 199 | listItems() { 200 | let items = [] 201 | for (const [task, status] of Object.entries(this.state.progress)) { 202 | let color = status[1] ? "bg-green-500" : "bg-yellow-400"; 203 | items.push( 204 | {task} 205 | {status[0]} 206 | ) 207 | } 208 | return items; 209 | } 210 | 211 | render() { 212 | return
213 |

Update progress:

214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | {this.listItems()} 223 | 224 |
Operation/categoryProgress
225 |
226 | } 227 | } 228 | 229 | function Container(props) { 230 | return
{props.children}
231 | } 232 | 233 | function Navbar() { 234 | return
235 | 238 | Component search 239 | 240 | 243 | Catalog history 244 | 245 |
246 | } 247 | 248 | export function NoMatch() { 249 | return

404 not found

; 250 | } 251 | 252 | class App extends React.Component { 253 | constructor(props) { 254 | super(props); 255 | this.state = { 256 | updating: false 257 | }; 258 | } 259 | 260 | onUpdateFinish = () => { 261 | this.setState({updating: false}); 262 | } 263 | 264 | triggerUpdate = () => { 265 | this.setState({updating: true}); 266 | } 267 | 268 | render() { 269 | if (this.state.updating) { 270 | return 271 | 272 | 273 | } 274 | return ( 275 | 276 | 277 | 278 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 |