partsdb

electronic parts inventory
git clone https://git.e1e0.net/partsdb.git
Log | Files | Refs

commit 2997e93fee451585aa0dc38232bed8cecac74585
Author: Paco Esteban <paco@e1e0.net>
Date:   Sat, 13 Mar 2021 10:32:26 +0100

initial commit

Diffstat:
A.gitignore | 3+++
Abase_data.sql | 5+++++
Adatabase.py | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AmyChips.py | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aoctopart.py | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aschema.sql | 31+++++++++++++++++++++++++++++++
6 files changed, 390 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +env +parts.db diff --git a/base_data.sql b/base_data.sql @@ -0,0 +1,5 @@ +INSERT INTO categories (name) VALUES ('Resistors'); +INSERT INTO categories (name) VALUES ('Capacitors'); +INSERT INTO categories (name) VALUES ('Diodes'); +INSERT INTO categories (name) VALUES (); +INSERT INTO categories (name) VALUES (); diff --git a/database.py b/database.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 + +import sqlite3 +from sqlite3 import Error + + +GET_CATEGORIES_QUERY = "SELECT * FROM categories" +GET_STORAGES_QUERY = "SELECT * FROM storages" +LIST_PARTS_CATEGORY_QUERY = """ + SELECT parts.id, parts.pn + FROM parts + INNER JOIN categories + ON parts.category_id = categories.id + WHERE categories.name = ? +""" +INSERT_PART_QUERY = """ + INSERT INTO parts + ( + pn, + mpn, + manufacturer, + description, + footprint, + category_id, + storage_id, + quantity, + datasheet, + image, + part_type + ) + VALUES (?,?,?,?,?,?,?,?,?,?,?) +""" +GET_PART_QUERY = "SELECT * FROM parts WHERE id = ?" +SEARCH_PARTS_QUERY = """ + SELECT * FROM parts + WHERE + pn LIKE '%'||?||'%' OR + mpn LIKE '%'||?||'%' OR + manufacturer LIKE '%'||?||'%' OR + footprint LIKE '%'||?||'%' OR + part_type LIKE '%'||?||'%' +""" + + +class PartsDB(): + + def __init__(self): + try: + self.conn = sqlite3.connect('parts.db') + except Error as e: + print(e) + + def get_categories(self): + c = self.conn.cursor() + c.execute(GET_CATEGORIES_QUERY) + return c.fetchall() + + def get_storages(self): + c = self.conn.cursor() + c.execute(GET_STORAGES_QUERY) + return c.fetchall() + + def list_parts_with_category(self, category): + c = self.conn.cursor() + c.execute(LIST_PARTS_CATEGORY_QUERY, (category,)) + return c.fetchall() + + def new_part(self, part): + if part[9] is not None: + part[9] = sqlite3.Binary(part[9]) + p = tuple(part) + with self.conn: + c = self.conn.cursor() + c.execute(INSERT_PART_QUERY, p) + + def get_part(self, part_id): + c = self.conn.cursor() + c.execute(GET_PART_QUERY, (part_id,)) + return c.fetchone() + + def search_parts(self, term): + c = self.conn.cursor() + c.execute(SEARCH_PARTS_QUERY, (term, term, term, term, term)) + return c.fetchall() + + def close(self): + self.conn.close() diff --git a/myChips.py b/myChips.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 + +import argparse +import os +import sys +import urllib.request + +from database import PartsDB +from octopart import OctopartClient + +octo = OctopartClient(os.getenv('OCTOPART_TOKEN', None)) +db = PartsDB() + + +def add_part(mpn): + result = octo.get_part(mpn)['data']['search']['results'] + if result is None: + print(f"Can't find results for {sys.argv[1]} on Octopart") + sys.exit(0) + + # list results from Octopart and pick one + for i, r in enumerate(result): + print('-'*79) + print(f"{i}\t{r['part']['manufacturer']['name']}") + print(f"\t{r['part']['short_description']}") + + pick = int(input("Which one seems better ? ")) + p = result[pick]['part'] + + # list categories to choose from + for c in db.get_categories(): + print(f"{c[0]}) {c[1]}") + category = int(input("In which category do you want it in ? ")) + + # list storages to choose from + for s in db.get_storages(): + print(f"{s[0]}) {s[1]}") + storage = int(input("Where will you store it ? ")) + + quantity = int(input("How many of them ? ")) + smd = input("Is this an SMD part (y/n, default yes) ? ") + + footprint = None + datasheet = None + image = None + part_type = 'smd' + + if 'specs' in p: + for s in p['specs']: + if s['attribute']['shortname'] == 'case_package': + footprint = s['display_value'] + + if 'best_datasheet' in p: + if p['best_datasheet']['mime_type'] == 'application/pdf': + datasheet = p['best_datasheet']['url'] + elif 'document_collections' in p: + for d in p['document_collections'][0]['documents']: + if d['mime_type'] == 'application/pdf' and d['name'] == 'Datasheet': + datasheet = d['url'] + headers = {} + headers['User-Agent'] = "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:48.0) Gecko/20100101 Firefox/48.0" + if datasheet is not None: + req = urllib.request.Request(datasheet, headers=headers) + datasheet = urllib.request.urlopen(req).read() + + if 'best_image' in p: + image = p['best_image']['url'] + if image is not None: + req = urllib.request.Request(image, headers=headers) + image = urllib.request.urlopen(req).read() + + if smd == 'n' or smd == 'N': + part_type = 'through-hole' + + part = [ + p['mpn'], + p['mpn'], + p['manufacturer']['name'], + p['short_description'], + footprint, + category, + storage, + quantity, + datasheet, + image, + part_type + ] + db.new_part(part) + + +def list_parts(category): + parts = db.list_parts_with_category(category) + + if parts is None: + print("There are no parts in this category") + + for p in parts: + print(f"{p[0]}) {p[1]}") + + +def search_part(search_term): + parts = db.search_parts(search_term) + + if parts is None: + print("No parts found") + + for p in parts: + print(f"{p[0]}) {p[1]}") + + +def get_part(part_id): + part = db.get_part(part_id) + print(part[1]) + + +def delete_part(part_id): + pass + + +if __name__ == '__main__': + ap = argparse.ArgumentParser() + # Place for global options here + # parser.add_argument(...) + # And then the commands + asp = ap.add_subparsers(dest="command") + # add + ap_add = asp.add_parser("add", help="Add new part from Octopart") + ap_add.add_argument("mpn", help="Manufacturer part number") + # list + ap_list = asp.add_parser("list", help="List all parts from a category") + ap_list.add_argument("category", help="Category Name") + # search + ap_search = asp.add_parser("search", help="Search for parts") + ap_search.add_argument("search_term", help="Term to search for") + # get + ap_get = asp.add_parser("get", help="Get all details for a part") + ap_get.add_argument("part_id", help="Part Id", type=int) + # delete + ap_delete = asp.add_parser("delete", help="Delete a part") + ap_delete.add_argument("part_id", help="Part Id", type=int) + + args = ap.parse_args() + if not args.command: + ap.print_help() + sys.exit(0) + + if args.command == 'add': + add_part(args.mpn) + elif args.command == 'list': + list_parts(args.category) + elif args.command == 'search': + search_part(args.search_term) + elif args.command == 'get': + get_part(args.part_id) + elif args.command == 'delete': + delete_part(args.part_id) + else: + ap.print_help() + sys.exit(0) + + db.close() diff --git a/octopart.py b/octopart.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 + +import json +import sys +import urllib.error +import urllib.request + + +GET_PARTS_GRAPHQL_QUERY = ''' + query MyPartSearch($q: String!, $filters: Map) { + search(q: $q, filters: $filters, limit: 2) { + total + results { + part { + id + mpn + aka_mpns + manufacturer { + name + } + short_description + specs { + attribute { + name + shortname + } + display_value + } + best_datasheet { + url + mime_type + } + best_image { + url + } + document_collections { + name + documents { + name + url + mime_type + } + } + } + } + } + } +''' + + +# copy pasted from: https://github.com/prisma-labs/python-graphql-client/blob/master/graphqlclient/client.py +class OctopartClient(): + + def __init__(self, token): + self.endpoint = 'https://octopart.com/api/v4/endpoint' + if token is None: + print('I need OCTOPART_TOKEN') + sys.exit(1) + self.token = token + + def execute(self, query, variables=None): + return self._send(query, variables) + + def _send(self, query, variables): + data = {'query': query, + 'variables': variables} + headers = {'Accept': 'application/json', + 'Content-Type': 'application/json'} + + if self.token is not None: + headers['token'] = '{}'.format(self.token) + + req = urllib.request.Request(self.endpoint, + json.dumps(data).encode('utf-8'), headers) + + try: + response = urllib.request.urlopen(req) + return response.read().decode('utf-8') + except urllib.error.HTTPError as e: + print((e.read())) + raise e + + def get_part(self, mpn): + query = GET_PARTS_GRAPHQL_QUERY + # parameters = { + # "q": "INA219AIDR", + # "filters": { + # "manufacturer_id": ["370"] + # } + # } + parameters = { + "q": mpn + } + + resp = self.execute(query, parameters) + # print(json.dumps(json.loads(resp), indent = 2)) + return json.loads(resp) diff --git a/schema.sql b/schema.sql @@ -0,0 +1,31 @@ +-- categories table +CREATE TABLE IF NOT EXISTS categories ( + id integer PRIMARY KEY, + name text NOT NULL +); + +-- storages table +CREATE TABLE IF NOT EXISTS storages ( + id integer PRIMARY KEY, + name text NOT NULL +); + +-- parts table +CREATE TABLE IF NOT EXISTS parts ( + id integer PRIMARY KEY, + pn text NOT NULL, + mpn text, + manufacturer text, + description text, + footprint text, + category_id integer NOT NULL, + storage_id integer NOT NULL, + quantity integer NOT NULL DEFAULT 0, + datasheet blob, + image blob, + part_type text NOT NULL DEFAULT 'smd', + insert_date text NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_date text NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories (id), + FOREIGN KEY (storage_id) REFERENCES storages (id) +);