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