Compare commits
	
		
			12 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 53cae2e3cc | |||
| 84149f75cf | |||
| 0aa17bfd40 | |||
| e1639ff8ba | |||
| 2b328d805e | |||
| d085335fc5 | |||
| a38dd669a8 | |||
| ace82a0f4d | |||
| 819115960b | |||
| 5bd06d892d | |||
| b14c855cc9 | |||
| f1a17cb4e7 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,3 +2,5 @@ | ||||
| partdb-labeler.bat | ||||
| partdb_labeler.egg-info | ||||
| build | ||||
| */__pycache__/* | ||||
| dist | ||||
|   | ||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2025 Scott Alfter | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
| @@ -25,19 +25,19 @@ printer via USB. | ||||
| Usage | ||||
| ----- | ||||
|  | ||||
| ```partdb-labeler -h``` will show you the available options. | ||||
| ```partdb_labeler -h``` will show you the available options. | ||||
|  | ||||
| For convenience, you might also consider adding a short shell script somewhere in your PATH that will call the Python module | ||||
| with your server configuration.  I use this (the API key is a read-only key I've publicized elsewhere): | ||||
|  | ||||
| ``` | ||||
| #!/usr/bin/env bash | ||||
| partdb-labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 $* | ||||
| partdb_labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 $* | ||||
| ``` | ||||
|  | ||||
| or the same, as a batch file on Windows: | ||||
|  | ||||
| ``` | ||||
| @echo off | ||||
| partdb-labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 %* | ||||
| partdb_labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 %* | ||||
| ``` | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from math import ceil, floor | ||||
| from PIL import Image | ||||
| import textwrap | ||||
| import argparse | ||||
| import sys | ||||
|  | ||||
| # make substitutions for characters not in CP437 | ||||
|  | ||||
| @@ -52,25 +53,25 @@ def esc(s): | ||||
| # render a line of text at coordinates | ||||
| # return coordinates of next line | ||||
|  | ||||
| def textline(s, loc, fontnum): | ||||
| def textline(z, res, s, loc, fontnum): | ||||
|     z.output(f"A{loc[0]},{loc[1]},0,{fontnum},1,1,N,\"{esc(filter(subst(s), "cp437"))}\"\n") | ||||
|     return (loc[0], loc[1]+font_metrics[fontnum][1]) | ||||
|     return (loc[0], loc[1]+font_metrics(res)[fontnum][1]) | ||||
|  | ||||
| # wrap text in a bounding box at coordinates | ||||
| # return coordinates of next line and any unused text | ||||
|  | ||||
| def textbox(s, loc, bbox, fontnum): | ||||
|     wrapped=textwrap.wrap(filter(subst(s), "cp437"), width=floor(bbox[0]/font_metrics[fontnum][0])) | ||||
| def textbox(z, res, s, loc, bbox, fontnum): | ||||
|     wrapped=textwrap.wrap(filter(subst(s), "cp437"), width=floor(bbox[0]/font_metrics(res)[fontnum][0])) | ||||
|     line=0 | ||||
|     while line*font_metrics[fontnum][1]<bbox[1] and line<len(wrapped): | ||||
|         loc=textline(wrapped[line], loc, fontnum) | ||||
|     while line*font_metrics(res)[fontnum][1]<bbox[1] and line<len(wrapped): | ||||
|         loc=textline(z, res, wrapped[line], loc, fontnum) | ||||
|         line=line+1 | ||||
|     return loc, " ".join(wrapped[line:]) | ||||
|  | ||||
| # render a QR code at coordinates | ||||
| # return size (single value, since QR codes are square) | ||||
|  | ||||
| def qr(s, loc, mul, brdr): | ||||
| def qr(z, s, loc, mul, brdr): | ||||
|     qr = qrcode.QRCode( | ||||
|         box_size=mul, | ||||
|         border=brdr, | ||||
| @@ -83,9 +84,30 @@ def qr(s, loc, mul, brdr): | ||||
|     z.output(f"GW10,10,{ceil(padded.width/8)},{padded.height},{padded.tobytes().decode("cp437")}\n") | ||||
|     return img.height | ||||
|  | ||||
|     # width and height for built-in monospace fonts (includes whitespace) | ||||
|  | ||||
| def font_metrics(res): | ||||
|     m={} | ||||
|     if res==203: | ||||
|         m[1]=(10,14) | ||||
|         m[2]=(12,18) | ||||
|         m[3]=(14,22) | ||||
|         m[4]=(16,26) | ||||
|         m[5]=(34,50) | ||||
|     if res==300: | ||||
|         m[1]=(14,22) | ||||
|         m[2]=(18,30) | ||||
|         m[3]=(22,38) | ||||
|         m[4]=(26,46) | ||||
|         m[5]=(50,82) | ||||
|     return m | ||||
|  | ||||
| # entrypoint | ||||
|  | ||||
| def cli(): | ||||
|     parser=argparse.ArgumentParser() | ||||
|     parser.add_argument("id", help="part ID", type=int) | ||||
|     parser.add_argument("id", help="part ID (or IPN)") | ||||
|     parser.add_argument("-i", action="store_true", help="search by IPN instead of part ID") | ||||
|     parser.add_argument("-x", help="label width, in inches (default: 2\")", type=float) | ||||
|     parser.add_argument("-y", help="label height, in inches (default: 1\")", type=float) | ||||
|     parser.add_argument("-g", help="label gap, in inches (default: 0.118\")", type=float) | ||||
| @@ -93,7 +115,7 @@ def cli(): | ||||
|     parser.add_argument("-p", help="PartDB base URL") | ||||
|     parser.add_argument("-k", help="PartDB API key") | ||||
|     parser.add_argument("-r", help="printer resolution (default: 203 dpi)", type=int) | ||||
|     parser.add_argument('-v', action='version', version='%(prog)s 0.1.2') | ||||
|     parser.add_argument('-v', action='version', version='%(prog)s 0.3.1') | ||||
|     args=parser.parse_args() | ||||
|     id=args.id | ||||
|     if args.x==None: | ||||
| @@ -121,30 +143,27 @@ def cli(): | ||||
|         if res!=203 and res!=300: | ||||
|             raise ValueError("valid resolution options are 203 and 300") | ||||
|  | ||||
|     # width and height for built-in monospace fonts (includes whitespace) | ||||
|  | ||||
|     font_metrics={} | ||||
|     if res==203: | ||||
|         font_metrics[1]=(10,14) | ||||
|         font_metrics[2]=(12,18) | ||||
|         font_metrics[3]=(14,22) | ||||
|         font_metrics[4]=(16,26) | ||||
|         font_metrics[5]=(34,50) | ||||
|     if res==300: | ||||
|         font_metrics[1]=(14,22) | ||||
|         font_metrics[2]=(18,30) | ||||
|         font_metrics[3]=(22,38) | ||||
|         font_metrics[4]=(26,46) | ||||
|         font_metrics[5]=(50,82) | ||||
|  | ||||
|     # look up the part | ||||
|  | ||||
|     url=f"{base_url}/api/parts/{id}" | ||||
|     if args.i==True: | ||||
|         url=f"{base_url}/api/parts/?ipn={id}" | ||||
|     else: | ||||
|         url=f"{base_url}/api/parts/{id}" | ||||
|     headers={} | ||||
|     headers["Accept"]="application/json" | ||||
|     if api_key!=None: | ||||
|         headers["Authorization"]=f"Bearer {api_key}" | ||||
|     part=requests.get(url, headers=headers).json() | ||||
|     if args.i==True: # search by IPN | ||||
|         try: | ||||
|             part=part[0]  | ||||
|         except IndexError: | ||||
|             sys.exit("part not found") | ||||
|     try: | ||||
|         if part["status"]==404: # catch a search that returns nothing | ||||
|             sys.exit("part not found") | ||||
|     except KeyError: | ||||
|         pass | ||||
|  | ||||
|     # render a label for it | ||||
|  | ||||
| @@ -159,19 +178,28 @@ def cli(): | ||||
|     z.output("N\n") | ||||
|  | ||||
|     if res==300: | ||||
|         qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 6, 3) | ||||
|         qr_size=qr(z, f"{base_url}/en/part/{id}", (10, 10), 6, 3) | ||||
|     else: | ||||
|         qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 4, 2) | ||||
|         qr_size=qr(z, f"{base_url}/en/part/{id}", (10, 10), 4, 2) | ||||
|  | ||||
|     loc=(15+qr_size, 20) | ||||
|     loc=textline(part["ipn"], loc, 5) | ||||
|     try: | ||||
|         loc=textline(z, res, part["ipn"], loc, 5) | ||||
|     except KeyError: | ||||
|         loc=textline(z, res, "", loc, 5) | ||||
|  | ||||
|     loc, excess=textbox(f"{part["manufacturer"]["name"]} {part["manufacturer_product_number"]}", loc, (label_width-loc[0]-10, label_height-loc[1]-10), 2) | ||||
|     try: | ||||
|         loc, excess=textbox(z, res, f"{part["manufacturer"]["name"]} {part["manufacturer_product_number"]}", loc, (label_width-loc[0]-10, label_height-loc[1]-10), 2) | ||||
|     except KeyError: | ||||
|         loc, excess=textbox(z, res, "", loc, (label_width-loc[0]-10, label_height-loc[1]-10), 2) | ||||
|  | ||||
|     avail_y=floor((20+qr_size-loc[1])/font_metrics[2][1])*font_metrics[2][1] | ||||
|     loc, excess=textbox(part["description"], loc, (label_width-loc[0]-10, avail_y), 2) | ||||
|     avail_y=floor((20+qr_size-loc[1])/font_metrics(res)[2][1])*font_metrics(res)[2][1] | ||||
|     loc, excess=textbox(z, res, part["description"], loc, (label_width-loc[0]-10, avail_y), 2) | ||||
|     if excess!="": | ||||
|         loc=(10, loc[1]) | ||||
|         loc, excess=textbox(excess, loc, (label_width-20, label_height-loc[1]), 2) | ||||
|         loc, excess=textbox(z, res, excess, loc, (label_width-20, label_height-loc[1]), 2) | ||||
|  | ||||
|     z.output("P1\n") | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     cli() | ||||
|   | ||||
							
								
								
									
										6
									
								
								publish.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								publish.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| # publish to PyPI | ||||
| rm -r dist || true | ||||
| python setup.py sdist bdist_wheel | ||||
| twine upload dist/* | ||||
							
								
								
									
										22
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| [build-system] | ||||
| requires = ["setuptools"] | ||||
| build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name = "partdb_labeler" | ||||
| version = "0.3.1" | ||||
| authors = [ | ||||
|     {name = "Scott Alfter", email = "scott@alfter.us"}, | ||||
| ] | ||||
| description = "PartDB Labeler" | ||||
| readme = "README.md" | ||||
| requires-python = ">=3.8" | ||||
| dependencies = [ | ||||
|         "requests",  | ||||
|         "zebra",  | ||||
|         "qrcode",  | ||||
|         "pillow" | ||||
| ] | ||||
|  | ||||
| [project.scripts] | ||||
| partdb_labeler = "partdb_labeler.partdb_labeler:cli" | ||||
							
								
								
									
										5
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								setup.cfg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| [metadata] | ||||
| description_file = README.md | ||||
| url = https://gitlab.alfter.us/salfter/partdb-labeler | ||||
| author = Scott Alfter | ||||
| author_email = scott@alfter.us | ||||
							
								
								
									
										24
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,24 +1,2 @@ | ||||
| from setuptools import setup | ||||
| import sys | ||||
|  | ||||
| with open("README.md", "r") as fh: | ||||
|     long_description = fh.read() | ||||
|  | ||||
| setup( | ||||
|     name="partdb_labeler", | ||||
|     version="0.2", | ||||
|     description="PartDB Labeler", | ||||
|     long_description=long_description, | ||||
|     long_description_content_type="text/markdown", | ||||
|     author="Scott Alfter", | ||||
|     author_email="scott@alfter.us", | ||||
|     url="https://gitlab.alfter.us/salfter/partdb-labeler", | ||||
|     py_modules=["partdb_labeler.partdb_labeler"], | ||||
|     install_requires=[ | ||||
|         "requests",  | ||||
|         "zebra",  | ||||
|         "qrcode",  | ||||
|         "pillow" | ||||
|     ], | ||||
|     entry_points={"console_scripts": ["partdb-labeler = partdb_labeler.partdb_labeler:cli"]} | ||||
| ) | ||||
| setup() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user