partsdb

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

partsdb.py (9521B)


      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.2.5'
     17 octo = oc.OctopartClient(os.getenv('OCTOPART_TOKEN', None))
     18 db = pdb.PartsDB(os.getenv('PARTSDB_FILE',
     19                            f"{os.getenv('HOME')}/.local/share/parts.db"))
     20 
     21 
     22 def add_part(mpn, quantity, category, storage, part_type):
     23     result = octo.get_part(mpn)['data']['search']['results']
     24     if result is None:
     25         print(f"Can't find results for {sys.argv[1]} on Octopart")
     26         sys.exit(0)
     27 
     28     # list results from Octopart and pick one
     29     for i, r in enumerate(result):
     30         print('-'*79)
     31         print(f"{i}\t{r['part']['manufacturer']['name']}"
     32               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:
     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['User-Agent'] = "Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:48.0) Gecko/20100101 Firefox/48.0"
     96     if datasheet is not None:
     97         req = urllib.request.Request(datasheet, headers=headers)
     98         datasheet = urllib.request.urlopen(req).read()
     99 
    100     if 'best_image' in p:
    101         image = p['best_image']['url']
    102     if image is not None:
    103         req = urllib.request.Request(image, headers=headers)
    104         image = urllib.request.urlopen(req).read()
    105 
    106     part = [
    107         p['mpn'],
    108         p['mpn'],
    109         p['manufacturer']['name'],
    110         p['short_description'],
    111         specs,
    112         footprint,
    113         category,
    114         storage,
    115         quantity,
    116         datasheet,
    117         image,
    118         part_type
    119     ]
    120     new_id = db.new_part(part)
    121     db.new_part_history_event(new_id, quantity, "first purchase")
    122 
    123 
    124 def list_parts(category, short):
    125     if category == 'all':
    126         parts = db.list_parts()
    127     else:
    128         parts = db.list_parts_by_category(category)
    129 
    130     if not parts:
    131         print("There are no parts in this category")
    132         return
    133 
    134     helpers.print_parts_list(parts, short)
    135 
    136 
    137 def search_part(search_term):
    138     parts = db.search_parts(search_term)
    139 
    140     if not parts:
    141         print("No parts found")
    142         return
    143 
    144     helpers.print_parts_list(parts)
    145 
    146 
    147 def get_part(part_id):
    148     part = db.get_part(part_id)
    149     history = db.get_part_history(part_id)
    150     helpers.print_part(part, history)
    151 
    152 
    153 def open_image(part_id):
    154     image = db.get_image(part_id)
    155     if image is None:
    156         print(f"There's no image for this part ID ({part_id})")
    157         return
    158     helpers.open_file(image['image'], '.jpg')
    159 
    160 
    161 def open_datasheet(part_id):
    162     datasheet = db.get_datasheet(part_id)
    163     if datasheet is None:
    164         print(f"There's no datasheet for this part ID ({part_id})")
    165         return
    166     helpers.open_file(datasheet['datasheet'], '.pdf')
    167 
    168 
    169 def delete_part(part_id):
    170     db.delete_part(part_id)
    171 
    172 
    173 def adjust_stock(part_id, stock_mod, comment):
    174     db.new_part_history_event(part_id, stock_mod, comment)
    175     db.update_part_qty(part_id, stock_mod)
    176 
    177 
    178 def export_db(dest_folder):
    179     env = Environment(
    180         loader=PackageLoader('partsdb.exports', 'templates'),
    181         autoescape=select_autoescape(['html', 'xml'])
    182     )
    183     categories = db.get_categories()
    184     helpers.html_main_index(dest_folder, categories, env)
    185     for c in categories:
    186         parts = db.list_parts_by_category(c['name'])
    187         helpers.html_category_index(dest_folder, c, parts, env)
    188     parts = db.list_parts()
    189     for p in parts:
    190         part = db.get_part(p['id'])
    191         history = db.get_part_history(p['id'])
    192         helpers.html_part(dest_folder, part, history, env)
    193         image = db.get_image(p['id'])
    194         datasheet = db.get_datasheet(p['id'])
    195         helpers.html_attachments(dest_folder, p['id'], datasheet, image)
    196     helpers.html_css(dest_folder, env)
    197 
    198 
    199 def list_categories():
    200     categories = db.get_categories()
    201     print("ID\tName")
    202     print("-"*40)
    203     for c in categories:
    204         print(f"{c['id']}\t{c['name']}")
    205 
    206 
    207 def main():
    208     ap = argparse.ArgumentParser()
    209     ap.add_argument('--version', '-v', action='version',
    210                     version='%(prog)s '+__version__)
    211     # Place for global options here
    212     # parser.add_argument(...)
    213     # And then the commands
    214     asp = ap.add_subparsers(dest="command")
    215     # add
    216     ap_add = asp.add_parser("add", help="Add new part from Octopart")
    217     ap_add.add_argument("mpn", help="Manufacturer part number")
    218     ap_add.add_argument("-q", dest='quantity',
    219                         help="Quantity of new items", type=int)
    220     ap_add.add_argument("-c", dest='category',
    221                         help="Which categoryId it belongs to", type=int)
    222     ap_add.add_argument("-s", dest='storage',
    223                         help="Which storageId it belongs to", type=int)
    224     ap_add.add_argument("-t", dest='type', choices=['smd', 'th'],
    225                         default='none', help="Trhough-hole of smd ?")
    226     # cat
    227     ap_cat = asp.add_parser("cat", help="List categories")
    228     # list
    229     ap_list = asp.add_parser("list",
    230                              help="List all parts from a category (or all)")
    231     ap_list.add_argument("category", help="Category Name")
    232     ap_list.add_argument("-s", dest='short',
    233                          action='store_true', help="Short output")
    234     # search
    235     ap_search = asp.add_parser("search", help="Search for parts")
    236     ap_search.add_argument("search_term", help="Term to search for")
    237     # get
    238     ap_get = asp.add_parser("get", help="Get all details for a part")
    239     ap_get.add_argument("part_id", help="Part Id", type=int)
    240     ap_get.add_argument("-d", dest='datasheet', action='store_true',
    241                         help="Open datasheet if available.")
    242     ap_get.add_argument("-i", dest='image', action='store_true',
    243                         help="Open image if available.")
    244     # delete
    245     ap_delete = asp.add_parser("delete", help="Delete a part")
    246     ap_delete.add_argument("part_id", help="Part Id", type=int)
    247     # stock
    248     ap_stock = asp.add_parser("stock", help="Modifies a part stock")
    249     ap_stock.add_argument("part_id", help="Part Id", type=int)
    250     ap_stock.add_argument("stock_mod",
    251                           help="Stock modifier (+ or -) int", type=int)
    252     ap_stock.add_argument("comment", help="Reason for the stock mod")
    253     # export
    254     ap_export = asp.add_parser("export", help="Exports DB to HTML")
    255     ap_export.add_argument("dest_folder", help="Destination folder")
    256 
    257     args = ap.parse_args()
    258     if not args.command:
    259         ap.print_help()
    260         sys.exit(0)
    261 
    262     if args.command == 'add':
    263         add_part(args.mpn, args.quantity, args.category, args.storage,
    264                  args.type)
    265     elif args.command == 'list':
    266         list_parts(args.category, args.short)
    267     elif args.command == 'search':
    268         search_part(args.search_term)
    269     elif args.command == 'get':
    270         get_part(args.part_id)
    271         if args.datasheet:
    272             open_datasheet(args.part_id)
    273         if args.image:
    274             open_image(args.part_id)
    275     elif args.command == 'delete':
    276         delete_part(args.part_id)
    277     elif args.command == 'stock':
    278         adjust_stock(args.part_id, args.stock_mod, args.comment)
    279     elif args.command == 'export':
    280         export_db(args.dest_folder)
    281     elif args.command == 'cat':
    282         list_categories()
    283     else:
    284         ap.print_help()
    285         sys.exit(0)
    286 
    287     db.close()
    288 
    289 
    290 if __name__ == '__main__':
    291     main()