partsdb

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

partsdb.py (9368B)


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