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()