partsdb

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

partsdb.py (10119B)


      1 #!/usr/bin/env python3
      2 # -*- coding: utf-8 -*-
      3 # vim:fenc=utf-8
      4 
      5 import argparse
      6 import os
      7 import sys
      8 import urllib.request
      9 
     10 from jinja2 import Environment, PackageLoader, select_autoescape
     11 
     12 from partsdb import database as pdb
     13 from partsdb import helpers
     14 from partsdb import octopart as oc
     15 
     16 __version__ = "v1.3.0"
     17 octo = oc.OctopartClient(os.getenv("OCTOPART_TOKEN", None))
     18 db = pdb.PartsDB(
     19     os.getenv("PARTSDB_FILE", f"{os.getenv('HOME')}/.local/share/partsdb/parts.db")
     20 )
     21 
     22 
     23 def add_part(mpn, quantity, category, storage, part_type):
     24     result = octo.get_part(mpn)["data"]["search"]["results"]
     25     if result is None:
     26         print(f"Can't find results for {sys.argv[1]} on Octopart")
     27         sys.exit(0)
     28 
     29     # list results from Octopart and pick one
     30     for i, r in enumerate(result):
     31         print("-" * 79)
     32         print(f"{i}\t{r['part']['manufacturer']['name']}" f"\t{r['part']['mpn']}")
     33         print(f"\t{r['part']['short_description']}")
     34         print("Sold by:")
     35         for s in r["part"]["sellers"]:
     36             print(s["company"]["name"])
     37         print(f"\t{r['part']['octopart_url']}")
     38 
     39     pick = int(input("Which one seems better ? "))
     40     p = result[pick]["part"]
     41 
     42     if quantity is None:
     43         quantity = int(input("How many of them ? "))
     44 
     45     # if this exists we increment stock
     46     spart = db.get_part_by_mpn(p["mpn"])
     47     if spart:
     48         db.update_part_qty(spart["id"], quantity)
     49         db.new_part_history_event(spart["id"], quantity, "new buy")
     50         return
     51 
     52     # only ask for categories if we do not have one already
     53     # list categories to choose from
     54     if category is None:
     55         for c in db.get_categories():
     56             print(f"{c['id']}) {c['name']}")
     57         category = int(input("In which category do you want it in ? "))
     58 
     59     # only ask for storage if we do not have one already
     60     # list storages to choose from
     61     if storage is None:
     62         for s in db.get_storages():
     63             print(f"{s['id']}) {s['name']}")
     64         storage = int(input("Where will you store it ? "))
     65 
     66     # only ask for part type if we do not have one already
     67     if part_type == "none":
     68         smd = input("Is this an SMD part (y/n, default yes) ? ")
     69         if smd == "n" or smd == "N":
     70             part_type = "th"
     71         else:
     72             part_type = "smd"
     73 
     74     footprint = None
     75     datasheet = None
     76     image = None
     77     specs = ""
     78 
     79     if "specs" in p:
     80         for s in p["specs"]:
     81             specs += f"{s['attribute']['name']}: {s['display_value']}\n"
     82             if s["attribute"]["shortname"] == "case_package":
     83                 footprint = s["display_value"]
     84     if not specs:
     85         specs = None
     86 
     87     if "best_datasheet" in p and p["best_datasheet"]:
     88         if p["best_datasheet"]["mime_type"] == "application/pdf":
     89             datasheet = p["best_datasheet"]["url"]
     90         elif "document_collections" in p:
     91             for d in p["document_collections"][0]["documents"]:
     92                 if d["mime_type"] == "application/pdf" and d["name"] == "Datasheet":
     93                     datasheet = d["url"]
     94     headers = {}
     95     headers[
     96         "User-Agent"
     97     ] = "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:48.0) Gecko/20100101 Firefox/48.0"
     98     if datasheet is not None:
     99         req = urllib.request.Request(datasheet, headers=headers)
    100         datasheet = urllib.request.urlopen(req).read()
    101 
    102     if "best_image" in p and p["best_image"]:
    103         image = p["best_image"]["url"]
    104     if image is not None:
    105         req = urllib.request.Request(image, headers=headers)
    106         image = urllib.request.urlopen(req).read()
    107 
    108     part = [
    109         p["mpn"],
    110         p["mpn"],
    111         p["manufacturer"]["name"],
    112         p["short_description"],
    113         specs,
    114         footprint,
    115         category,
    116         storage,
    117         quantity,
    118         datasheet,
    119         image,
    120         part_type,
    121     ]
    122     new_id = db.new_part(part)
    123     db.new_part_history_event(new_id, quantity, "first purchase")
    124 
    125 
    126 def list_parts(category, short):
    127     if category == "all":
    128         parts = db.list_parts()
    129     else:
    130         parts = db.list_parts_by_category(category)
    131 
    132     if not parts:
    133         print("There are no parts in this category")
    134         return
    135 
    136     helpers.print_parts_list(parts, short)
    137 
    138 
    139 def search_part(search_term, output):
    140     parts = db.search_parts(search_term)
    141 
    142     if not parts:
    143         print("No parts found")
    144         return
    145 
    146     helpers.print_parts_list(parts, output)
    147 
    148 
    149 def get_part(part_id, output):
    150     part = db.get_part(part_id)
    151     history = db.get_part_history(part_id)
    152     helpers.print_part(part, history, output)
    153 
    154 
    155 def open_image(part_id):
    156     image = db.get_image(part_id)
    157     if not image["image"]:
    158         print(f"There's no image for this part ID ({part_id})")
    159         return
    160     helpers.open_file(image["image"], ".jpg")
    161 
    162 
    163 def open_datasheet(part_id):
    164     datasheet = db.get_datasheet(part_id)
    165     if not datasheet["datasheet"]:
    166         print(f"There's no datasheet for this part ID ({part_id})")
    167         return
    168     helpers.open_file(datasheet["datasheet"], ".pdf")
    169 
    170 
    171 def delete_part(part_id):
    172     db.delete_part(part_id)
    173 
    174 
    175 def adjust_stock(part_id, stock_mod, comment):
    176     db.new_part_history_event(part_id, stock_mod, comment)
    177     db.update_part_qty(part_id, stock_mod)
    178 
    179 
    180 def export_db(dest_folder):
    181     env = Environment(
    182         loader=PackageLoader("partsdb.exports", "templates"),
    183         autoescape=select_autoescape(["html", "xml"]),
    184     )
    185     categories = db.get_categories()
    186     storages = db.get_storages()
    187     helpers.html_main_index(dest_folder, categories, storages, env)
    188     for s in categories:
    189         parts = db.list_parts_by_category(s["name"])
    190         helpers.html_category_index(dest_folder, s, parts, env)
    191     for s in storages:
    192         parts = db.list_parts_by_storage(s["name"])
    193         helpers.html_storage_index(dest_folder, s, parts, env)
    194     parts = db.list_parts()
    195     for p in parts:
    196         part = db.get_part(p["id"])
    197         history = db.get_part_history(p["id"])
    198         helpers.html_part(dest_folder, part, history, env)
    199         image = db.get_image(p["id"])
    200         datasheet = db.get_datasheet(p["id"])
    201         helpers.html_attachments(dest_folder, p["id"], datasheet, image)
    202     helpers.html_css(dest_folder, env)
    203 
    204 
    205 def list_categories():
    206     categories = db.get_categories()
    207     print("ID\tName")
    208     print("-" * 40)
    209     for c in categories:
    210         print(f"{c['id']}\t{c['name']}")
    211 
    212 
    213 def main():
    214     ap = argparse.ArgumentParser()
    215     ap.add_argument(
    216         "--version", "-v", action="version", version="%(prog)s " + __version__
    217     )
    218     # Place for global options here
    219     # parser.add_argument(...)
    220     # And then the commands
    221     asp = ap.add_subparsers(dest="command")
    222     # add
    223     ap_add = asp.add_parser("add", help="Add new part from Octopart")
    224     ap_add.add_argument("mpn", help="Manufacturer part number")
    225     ap_add.add_argument("-q", dest="quantity", help="Quantity of new items", type=int)
    226     ap_add.add_argument(
    227         "-c", dest="category", help="Which categoryId it belongs to", type=int
    228     )
    229     ap_add.add_argument(
    230         "-s", dest="storage", help="Which storageId it belongs to", type=int
    231     )
    232     ap_add.add_argument(
    233         "-t",
    234         dest="type",
    235         choices=["smd", "th"],
    236         default="none",
    237         help="Trhough-hole of smd ?",
    238     )
    239     # cat
    240     asp.add_parser("cat", help="List categories")
    241     # list
    242     ap_list = asp.add_parser("list", help="List all parts from a category (or all)")
    243     ap_list.add_argument("category", help="Category Name or ID")
    244     ap_list.add_argument(
    245         "-o",
    246         dest="output",
    247         choices=["full", "short", "json"],
    248         default="full",
    249         help="Short output",
    250     )
    251     # search
    252     ap_search = asp.add_parser("search", help="Search for parts")
    253     ap_search.add_argument("search_term", help="Term to search for")
    254     ap_search.add_argument(
    255         "-o",
    256         dest="output",
    257         choices=["full", "short", "json"],
    258         default="full",
    259         help="Short output",
    260     )
    261     # get
    262     ap_get = asp.add_parser("get", help="Get all details for a part")
    263     ap_get.add_argument("part_id", help="Part Id", type=int)
    264     ap_get.add_argument(
    265         "-d", dest="datasheet", action="store_true", help="Open datasheet if available."
    266     )
    267     ap_get.add_argument(
    268         "-i", dest="image", action="store_true", help="Open image if available."
    269     )
    270     ap_get.add_argument(
    271         "-o",
    272         dest="output",
    273         choices=["full", "json"],
    274         default="full",
    275         help="Short output",
    276     )
    277     # delete
    278     ap_delete = asp.add_parser("delete", help="Delete a part")
    279     ap_delete.add_argument("part_id", help="Part Id", type=int)
    280     # stock
    281     ap_stock = asp.add_parser("stock", help="Modifies a part stock")
    282     ap_stock.add_argument("part_id", help="Part Id", type=int)
    283     ap_stock.add_argument("stock_mod", help="Stock modifier (+ or -) int", type=int)
    284     ap_stock.add_argument("comment", help="Reason for the stock mod")
    285     # export
    286     ap_export = asp.add_parser("export", help="Exports DB to HTML")
    287     ap_export.add_argument("dest_folder", help="Destination folder")
    288 
    289     args = ap.parse_args()
    290     if not args.command:
    291         ap.print_help()
    292         sys.exit(0)
    293 
    294     if args.command == "add":
    295         add_part(args.mpn, args.quantity, args.category, args.storage, args.type)
    296     elif args.command == "list":
    297         list_parts(args.category, args.output)
    298     elif args.command == "search":
    299         search_part(args.search_term, args.output)
    300     elif args.command == "get":
    301         get_part(args.part_id, args.output)
    302         if args.datasheet and args.output != "json":
    303             open_datasheet(args.part_id)
    304         if args.image and args.output != "json":
    305             open_image(args.part_id)
    306     elif args.command == "delete":
    307         delete_part(args.part_id)
    308     elif args.command == "stock":
    309         adjust_stock(args.part_id, args.stock_mod, args.comment)
    310     elif args.command == "export":
    311         export_db(args.dest_folder)
    312     elif args.command == "cat":
    313         list_categories()
    314     else:
    315         ap.print_help()
    316         sys.exit(0)
    317 
    318     db.close()
    319 
    320 
    321 if __name__ == "__main__":
    322     main()