partsdb

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

commit 5436f0a1f310a06c77ffffe1b728214afc53c99e
parent c9d5db815249730a3ee61fa153c3f782c2e2e2cb
Author: Paco Esteban <paco@e1e0.net>
Date:   Sat, 29 May 2021 10:09:42 +0200

create distribution package from this mess

Diffstat:
M.gitignore | 2++
RREADME -> README.md | 0
Dpartsdb.py | 283-------------------------------------------------------------------------------
Asetup.py | 41+++++++++++++++++++++++++++++++++++++++++
Rexports/__init__.py -> src/partsdb/__init__.py | 0
Rdatabase.py -> src/partsdb/database.py | 0
Rexports/__init__.py -> src/partsdb/exports/__init__.py | 0
Rexports/templates/cat.html -> src/partsdb/exports/templates/cat.html | 0
Rexports/templates/index.html -> src/partsdb/exports/templates/index.html | 0
Rexports/templates/part.html -> src/partsdb/exports/templates/part.html | 0
Rexports/templates/style.css -> src/partsdb/exports/templates/style.css | 0
Rhelpers.py -> src/partsdb/helpers.py | 0
Roctopart.py -> src/partsdb/octopart.py | 0
Asrc/partsdb/partsdb.py | 287+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
14 files changed, 330 insertions(+), 283 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,3 +2,5 @@ __pycache__ env parts.db migration.py +build +dist diff --git a/README b/README.md diff --git a/partsdb.py b/partsdb.py @@ -1,283 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# vim:fenc=utf-8 - -import argparse -import os -import sys -import urllib.request - -from jinja2 import Environment, PackageLoader, select_autoescape - -from database import PartsDB -from helpers import * -from octopart import OctopartClient - -__version__ = 'v0.1.0' -octo = OctopartClient(os.getenv('OCTOPART_TOKEN', None)) -db = PartsDB(os.getenv('PARTSDB_FILE', - f"{os.getenv('HOME')}/.local/share/parts.db")) - - -def add_part(mpn, quantity, category, storage, part_type): - 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']}" - f"\t{r['part']['mpn']}") - print(f"\t{r['part']['short_description']}") - - pick = int(input("Which one seems better ? ")) - p = result[pick]['part'] - - if quantity is None: - quantity = int(input("How many of them ? ")) - - # if this exists we increment stock - spart = db.get_part_by_mpn(p['mpn']) - if spart: - db.update_part_qty(spart['id'], quantity) - db.new_part_history_event(spart['id'], quantity, "new buy") - return - - # only ask for categories if we do not have one already - # list categories to choose from - if category is None: - for c in db.get_categories(): - print(f"{c['id']}) {c['name']}") - category = int(input("In which category do you want it in ? ")) - - # only ask for storage if we do not have one already - # list storages to choose from - if storage is None: - for s in db.get_storages(): - print(f"{s['id']}) {s['name']}") - storage = int(input("Where will you store it ? ")) - - # only ask for part type if we do not have one already - if part_type == 'none': - smd = input("Is this an SMD part (y/n, default yes) ? ") - if smd == 'n' or smd == 'N': - part_type = 'th' - else: - part_type = 'smd' - - footprint = None - datasheet = None - image = None - specs = '' - - if 'specs' in p: - for s in p['specs']: - specs += f"{s['attribute']['name']}: {s['display_value']}\n" - if s['attribute']['shortname'] == 'case_package': - footprint = s['display_value'] - if not specs: - specs = None - - 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() - - part = [ - p['mpn'], - p['mpn'], - p['manufacturer']['name'], - p['short_description'], - specs, - footprint, - category, - storage, - quantity, - datasheet, - image, - part_type - ] - new_id = db.new_part(part) - db.new_part_history_event(new_id, quantity, "first purchase") - - -def list_parts(category, short): - if category == 'all': - parts = db.list_parts() - else: - parts = db.list_parts_by_category(category) - - if not parts: - print("There are no parts in this category") - return - - print_parts_list(parts, short) - - -def search_part(search_term): - parts = db.search_parts(search_term) - - if not parts: - print("No parts found") - return - - print_parts_list(parts) - - -def get_part(part_id): - part = db.get_part(part_id) - history = db.get_part_history(part_id) - print_part(part, history) - - -def open_image(part_id): - image = db.get_image(part_id) - if image is None: - print(f"There's no image for this part ID ({part_id})") - return - open_file(image['image'], '.jpg') - - -def open_datasheet(part_id): - datasheet = db.get_datasheet(part_id) - if datasheet is None: - print(f"There's no datasheet for this part ID ({part_id})") - return - open_file(datasheet['datasheet'], '.pdf') - - -def delete_part(part_id): - db.delete_part(part_id) - - -def adjust_stock(part_id, stock_mod, comment): - db.new_part_history_event(part_id, stock_mod, comment) - db.update_part_qty(part_id, stock_mod) - - -def export_db(dest_folder): - env = Environment( - loader=PackageLoader('exports', 'templates'), - autoescape=select_autoescape(['html', 'xml']) - ) - categories = db.get_categories() - html_main_index(dest_folder, categories, env) - for c in categories: - parts = db.list_parts_by_category(c['name']) - html_category_index(dest_folder, c, parts, env) - parts = db.list_parts() - for p in parts: - part = db.get_part(p['id']) - history = db.get_part_history(p['id']) - html_part(dest_folder, part, history, env) - image = db.get_image(p['id']) - datasheet = db.get_datasheet(p['id']) - html_attachments(dest_folder, p['id'], datasheet, image) - html_css(dest_folder, env) - - -def list_categories(): - categories = db.get_categories() - print("ID\tName") - print("-"*40) - for c in categories: - print(f"{c['id']}\t{c['name']}") - - -if __name__ == '__main__': - ap = argparse.ArgumentParser() - ap.add_argument('--version', '-v', action='version', - version='%(prog)s '+__version__) - # 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") - ap_add.add_argument("-q", dest='quantity', - help="Quantity of new items", type=int) - ap_add.add_argument("-c", dest='category', - help="Which categoryId it belongs to", type=int) - ap_add.add_argument("-s", dest='storage', - help="Which storageId it belongs to", type=int) - ap_add.add_argument("-t", dest='type', choices=['smd', 'th'], - default='none', help="Trhough-hole of smd ?") - # cat - ap_cat = asp.add_parser("cat", help="List categories") - # list - ap_list = asp.add_parser("list", - help="List all parts from a category (or all)") - ap_list.add_argument("category", help="Category Name") - ap_list.add_argument("-s", dest='short', - action='store_true', help="Short output") - # 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) - ap_get.add_argument("-d", dest='datasheet', action='store_true', - help="Open datasheet if available.") - ap_get.add_argument("-i", dest='image', action='store_true', - help="Open image if available.") - # delete - ap_delete = asp.add_parser("delete", help="Delete a part") - ap_delete.add_argument("part_id", help="Part Id", type=int) - # stock - ap_stock = asp.add_parser("stock", help="Modifies a part stock") - ap_stock.add_argument("part_id", help="Part Id", type=int) - ap_stock.add_argument("stock_mod", - help="Stock modifier (+ or -) int", type=int) - ap_stock.add_argument("comment", help="Reason for the stock mod") - # export - ap_export = asp.add_parser("export", help="Exports DB to HTML") - ap_export.add_argument("dest_folder", help="Destination folder") - - args = ap.parse_args() - if not args.command: - ap.print_help() - sys.exit(0) - - if args.command == 'add': - add_part(args.mpn, args.quantity, args.category, args.storage, - args.type) - elif args.command == 'list': - list_parts(args.category, args.short) - elif args.command == 'search': - search_part(args.search_term) - elif args.command == 'get': - get_part(args.part_id) - if args.datasheet: - open_datasheet(args.part_id) - if args.image: - open_image(args.part_id) - elif args.command == 'delete': - delete_part(args.part_id) - elif args.command == 'stock': - adjust_stock(args.part_id, args.stock_mod, args.comment) - elif args.command == 'export': - export_db(args.dest_folder) - elif args.command == 'cat': - list_categories() - else: - ap.print_help() - sys.exit(0) - - db.close() diff --git a/setup.py b/setup.py @@ -0,0 +1,41 @@ +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +long_description = (here / 'README.md').read_text(encoding='utf-8') + +setup( + name='partsdb', + version='1.0.0', + description='Electronic parts database', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://git.e1e0.net/partsdb/', + author='Paco Esteban', + author_email='paco@e1e0.net', + classifiers=[ # Optional + 'Development Status :: 3 - Alpha', + 'License :: OSI Approved :: ISC License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3 :: Only', + ], + keywords='electronics, parts, database', + package_dir={'': 'src'}, + packages=find_packages(where='src'), + python_requires='>=3.6, <4', + install_requires=['jinja2'], + package_data={ + 'partsdb.exports': ['templates/*html', 'templates/style.css'], + }, + data_files=[('sql', ['schema.sql', 'base_data.sql'])], + entry_points={ # Optional + 'console_scripts': [ + 'partsdb=partsdb.partsdb:main', + ], + }, +) diff --git a/exports/__init__.py b/src/partsdb/__init__.py diff --git a/database.py b/src/partsdb/database.py diff --git a/exports/__init__.py b/src/partsdb/exports/__init__.py diff --git a/exports/templates/cat.html b/src/partsdb/exports/templates/cat.html diff --git a/exports/templates/index.html b/src/partsdb/exports/templates/index.html diff --git a/exports/templates/part.html b/src/partsdb/exports/templates/part.html diff --git a/exports/templates/style.css b/src/partsdb/exports/templates/style.css diff --git a/helpers.py b/src/partsdb/helpers.py diff --git a/octopart.py b/src/partsdb/octopart.py diff --git a/src/partsdb/partsdb.py b/src/partsdb/partsdb.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 + +import argparse +import os +import sys +import urllib.request + +from jinja2 import Environment, PackageLoader, select_autoescape + +from partsdb import database as pdb +from partsdb import helpers +from partsdb import octopart as oc + +__version__ = 'v1.0.0' +octo = oc.OctopartClient(os.getenv('OCTOPART_TOKEN', None)) +db = pdb.PartsDB(os.getenv('PARTSDB_FILE', + f"{os.getenv('HOME')}/.local/share/parts.db")) + + +def add_part(mpn, quantity, category, storage, part_type): + 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']}" + f"\t{r['part']['mpn']}") + print(f"\t{r['part']['short_description']}") + + pick = int(input("Which one seems better ? ")) + p = result[pick]['part'] + + if quantity is None: + quantity = int(input("How many of them ? ")) + + # if this exists we increment stock + spart = db.get_part_by_mpn(p['mpn']) + if spart: + db.update_part_qty(spart['id'], quantity) + db.new_part_history_event(spart['id'], quantity, "new buy") + return + + # only ask for categories if we do not have one already + # list categories to choose from + if category is None: + for c in db.get_categories(): + print(f"{c['id']}) {c['name']}") + category = int(input("In which category do you want it in ? ")) + + # only ask for storage if we do not have one already + # list storages to choose from + if storage is None: + for s in db.get_storages(): + print(f"{s['id']}) {s['name']}") + storage = int(input("Where will you store it ? ")) + + # only ask for part type if we do not have one already + if part_type == 'none': + smd = input("Is this an SMD part (y/n, default yes) ? ") + if smd == 'n' or smd == 'N': + part_type = 'th' + else: + part_type = 'smd' + + footprint = None + datasheet = None + image = None + specs = '' + + if 'specs' in p: + for s in p['specs']: + specs += f"{s['attribute']['name']}: {s['display_value']}\n" + if s['attribute']['shortname'] == 'case_package': + footprint = s['display_value'] + if not specs: + specs = None + + 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() + + part = [ + p['mpn'], + p['mpn'], + p['manufacturer']['name'], + p['short_description'], + specs, + footprint, + category, + storage, + quantity, + datasheet, + image, + part_type + ] + new_id = db.new_part(part) + db.new_part_history_event(new_id, quantity, "first purchase") + + +def list_parts(category, short): + if category == 'all': + parts = db.list_parts() + else: + parts = db.list_parts_by_category(category) + + if not parts: + print("There are no parts in this category") + return + + helpers.print_parts_list(parts, short) + + +def search_part(search_term): + parts = db.search_parts(search_term) + + if not parts: + print("No parts found") + return + + helpers.print_parts_list(parts) + + +def get_part(part_id): + part = db.get_part(part_id) + history = db.get_part_history(part_id) + helpers.print_part(part, history) + + +def open_image(part_id): + image = db.get_image(part_id) + if image is None: + print(f"There's no image for this part ID ({part_id})") + return + helpers.open_file(image['image'], '.jpg') + + +def open_datasheet(part_id): + datasheet = db.get_datasheet(part_id) + if datasheet is None: + print(f"There's no datasheet for this part ID ({part_id})") + return + helpers.open_file(datasheet['datasheet'], '.pdf') + + +def delete_part(part_id): + db.delete_part(part_id) + + +def adjust_stock(part_id, stock_mod, comment): + db.new_part_history_event(part_id, stock_mod, comment) + db.update_part_qty(part_id, stock_mod) + + +def export_db(dest_folder): + env = Environment( + loader=PackageLoader('exports', 'templates'), + autoescape=select_autoescape(['html', 'xml']) + ) + categories = db.get_categories() + helpers.html_main_index(dest_folder, categories, env) + for c in categories: + parts = db.list_parts_by_category(c['name']) + helpers.html_category_index(dest_folder, c, parts, env) + parts = db.list_parts() + for p in parts: + part = db.get_part(p['id']) + history = db.get_part_history(p['id']) + helpers.html_part(dest_folder, part, history, env) + image = db.get_image(p['id']) + datasheet = db.get_datasheet(p['id']) + helpers.html_attachments(dest_folder, p['id'], datasheet, image) + helpers.html_css(dest_folder, env) + + +def list_categories(): + categories = db.get_categories() + print("ID\tName") + print("-"*40) + for c in categories: + print(f"{c['id']}\t{c['name']}") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--version', '-v', action='version', + version='%(prog)s '+__version__) + # 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") + ap_add.add_argument("-q", dest='quantity', + help="Quantity of new items", type=int) + ap_add.add_argument("-c", dest='category', + help="Which categoryId it belongs to", type=int) + ap_add.add_argument("-s", dest='storage', + help="Which storageId it belongs to", type=int) + ap_add.add_argument("-t", dest='type', choices=['smd', 'th'], + default='none', help="Trhough-hole of smd ?") + # cat + ap_cat = asp.add_parser("cat", help="List categories") + # list + ap_list = asp.add_parser("list", + help="List all parts from a category (or all)") + ap_list.add_argument("category", help="Category Name") + ap_list.add_argument("-s", dest='short', + action='store_true', help="Short output") + # 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) + ap_get.add_argument("-d", dest='datasheet', action='store_true', + help="Open datasheet if available.") + ap_get.add_argument("-i", dest='image', action='store_true', + help="Open image if available.") + # delete + ap_delete = asp.add_parser("delete", help="Delete a part") + ap_delete.add_argument("part_id", help="Part Id", type=int) + # stock + ap_stock = asp.add_parser("stock", help="Modifies a part stock") + ap_stock.add_argument("part_id", help="Part Id", type=int) + ap_stock.add_argument("stock_mod", + help="Stock modifier (+ or -) int", type=int) + ap_stock.add_argument("comment", help="Reason for the stock mod") + # export + ap_export = asp.add_parser("export", help="Exports DB to HTML") + ap_export.add_argument("dest_folder", help="Destination folder") + + args = ap.parse_args() + if not args.command: + ap.print_help() + sys.exit(0) + + if args.command == 'add': + add_part(args.mpn, args.quantity, args.category, args.storage, + args.type) + elif args.command == 'list': + list_parts(args.category, args.short) + elif args.command == 'search': + search_part(args.search_term) + elif args.command == 'get': + get_part(args.part_id) + if args.datasheet: + open_datasheet(args.part_id) + if args.image: + open_image(args.part_id) + elif args.command == 'delete': + delete_part(args.part_id) + elif args.command == 'stock': + adjust_stock(args.part_id, args.stock_mod, args.comment) + elif args.command == 'export': + export_db(args.dest_folder) + elif args.command == 'cat': + list_categories() + else: + ap.print_help() + sys.exit(0) + + db.close() + + +if __name__ == '__main__': + main()