From ce74278bf6d92b328afae5210065c9587d56a14d Mon Sep 17 00:00:00 2001 From: jaseg Date: Tue, 9 Dec 2025 19:22:47 +0100 Subject: [PATCH] Kicad addon WIP --- kicad-plugin/__init__.py | 10 + kicad-plugin/icon-dark.png | Bin 0 -> 3555 bytes kicad-plugin/icon-dark.svg | 406 ++++++++++++++++++++++++++++++++++++ kicad-plugin/icon-light.png | Bin 0 -> 3452 bytes kicad-plugin/icon-light.svg | 406 ++++++++++++++++++++++++++++++++++++ kicad-plugin/plugin.json | 22 ++ metadata.json | 75 ++++++- package.py | 109 +++++----- pyproject.toml | 6 +- src/kicoil/gui.py | 123 +++++++++-- uv.lock | 57 +++++ 11 files changed, 1135 insertions(+), 79 deletions(-) create mode 100644 kicad-plugin/__init__.py create mode 100644 kicad-plugin/icon-dark.png create mode 100644 kicad-plugin/icon-dark.svg create mode 100644 kicad-plugin/icon-light.png create mode 100644 kicad-plugin/icon-light.svg create mode 100644 kicad-plugin/plugin.json diff --git a/kicad-plugin/__init__.py b/kicad-plugin/__init__.py new file mode 100644 index 0000000..b932c00 --- /dev/null +++ b/kicad-plugin/__init__.py @@ -0,0 +1,10 @@ +from pathlib import Path + +from kipy import KiCad +from kipy.errors import ConnectionError + +import kicoil.gui + +if __name__ == '__main__': + kicad_inst = KiCad() + kicoil.gui.main(kicad_inst) diff --git a/kicad-plugin/icon-dark.png b/kicad-plugin/icon-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..93d143cf618878e38b18d8291106650dc08d034d GIT binary patch literal 3555 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kicad-plugin/icon-light.png b/kicad-plugin/icon-light.png new file mode 100644 index 0000000000000000000000000000000000000000..fcf9dcd2d60f064212913d0133f343d970f457ae GIT binary patch literal 3452 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 literal 0 HcmV?d00001 diff --git a/kicad-plugin/icon-light.svg b/kicad-plugin/icon-light.svg new file mode 100644 index 0000000..6c4dc13 --- /dev/null +++ b/kicad-plugin/icon-light.svg @@ -0,0 +1,406 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kicad-plugin/plugin.json b/kicad-plugin/plugin.json new file mode 100644 index 0000000..c367b8e --- /dev/null +++ b/kicad-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://go.kicad.org/api/schemas/v1", + "identifier": "de.jaseg.kicoil", + "name": "KiCoil", + "description": "Planar inductor supporting spiral coils, toroidal coils, and hybrids", + "runtime": { + "type": "python", + "min_version": 3.13 + }, + "actions": [ + { + "identifier": "kicoil-generator", + "name": "KiCoil Planar Inductor Generator", + "description": "Generates planar spiral inductors, toroidal inductors, and hybrids thereof", + "show-button": true, + "scopes": ["pcb"], + "entrypoint": "kicoil_plugin_action.py", + "icons-light": ["icon-light.png"], + "icons-dark": ["icon-dark.png"] + } + ] +} diff --git a/metadata.json b/metadata.json index d15c29a..91e0e21 100644 --- a/metadata.json +++ b/metadata.json @@ -1,7 +1,7 @@ { "$schema": "https://go.kicad.org/pcm/schemas/v1", "name": "KiCoil", - "description": "A planar inductor for KiCad", + "description": "Planar inductor supporting spiral coils, toroidal coils, and hybrids", "description_full": "KiCoil generates planar inductors as footprints. Currently, circular spiral and toroid inductors are supported. KiCoil supports arbitrary intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower parasitics and higher self-resonant frequency than standard multilayer spiral inductors.", "identifier": "de.jaseg.kicoil", "type": "plugin", @@ -11,7 +11,7 @@ "web": "https://jaseg.de/" } }, - "license": "GPL-3.0", + "license": "Apache-2.0", "resources": { "homepage": "https://jaseg.de/projects/kicoil", "git": "https://git.jaseg.de/kicoil", @@ -21,8 +21,75 @@ { "version": "0.9.0", "status": "stable", - "kicad_version": "8.00" + "kicad_version": "9.00", + "download_sha256": "e0696023ef669b1d4efac3a5cb47abb859db32dca1e3f5efd55344daa848a2be", + "download_size": 70143, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 196790 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "280139c454396d0afaa71ac7bd35c6c09c3c602c3ad55e77b14b51f22df16818", + "download_size": 70143, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 196790 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "3cf67dff3226ca5f3eb2409f5230b4bb02f5266351621b9e97c80493cc48ae61", + "download_size": 69827, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 196790 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "ae1144223256c1f59fad76c27dbdfa3722a16df162b9c3d13cd4e840481626e7", + "download_size": 69103, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 195410 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "429d93b5850bd54a767b5a18fa841c8b17ed30823568d934706d67dca38e1eb1", + "download_size": 69108, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 195414 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "538f89db2c4beef1c344260634b0b6f07c0ba7cb3a81fd71a9a9af028a5c38fe", + "download_size": 69703, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 196520 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "55a3a05e5730713c3456b45e6c8bca5f66e407944d1a465949547a3d5a2b7007", + "download_size": 72082, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 202369 + }, + { + "version": "0.9.0", + "status": "stable", + "kicad_version": "9.00", + "download_sha256": "736bd91edabc24fa1f342e98e56fa03796363f7e8240adeaaa461bd130ab7c57", + "download_size": 72070, + "download_url": "https://git.jaseg.de/kimesh.git/plain/de.jaseg.kicoil-v0.9.0.zip?h=v0.9.0", + "install_size": 202424 } ], "runtime": "ipc" -} +} \ No newline at end of file diff --git a/package.py b/package.py index a07ed60..8cd6a7f 100644 --- a/package.py +++ b/package.py @@ -15,8 +15,9 @@ def tree_size(path): @click.option('--major', 'increment', flag_value='major') @click.option('--minor', 'increment', flag_value='minor', default=True) @click.option('--patch', 'increment', flag_value='patch', default=True) +@click.option('--dry-run', is_flag=True) @click.argument('version', required=False) -def do_release(version, increment): +def do_release(version, increment, dry_run): if not version: tag = subprocess.run('git describe --tags --abbrev=0 --match v*'.split(), check=True, capture_output=True, text=True) @@ -30,77 +31,77 @@ def do_release(version, increment): major, minor, patch = (major, minor, patch+1) version = f'{major}.{minor}.{patch}' - res = subprocess.run('git status --porcelain --untracked-files=no'.split(), - check=True, capture_output=True, text=True) - if res.stdout.strip(): - raise click.ClickException('There are uncommitted changes in this repository.') + if not dry_run: + res = subprocess.run('git status --porcelain --untracked-files=no'.split(), + check=True, capture_output=True, text=True) + if res.stdout.strip(): + raise click.ClickException('There are uncommitted changes in this repository.') - print('Cleaning old footprints') - footprint_dir = Path('de.jaseg.kimesh.footprints') / 'footprints' - shutil.rmtree(footprint_dir, ignore_errors=True) - footprint_dir.mkdir() - - print('Re-generating footprints') - for n in range(1, 9): - subprocess.run(['python', '-m', 'footprint_generator', - '-w', '0.100,0.120,0.150,0.200,0.250,0.300,0.350,0.400,0.500,0.600,0.700,0.800,1.000,1.200,1.500,1.800', - '-c', '0.100,0.120,0.150,0.200,0.300,0.400,0.500', - '-n', str(n), - str(footprint_dir / f'kimesh_anchors_{n}wire.pretty') - ], check=True) + project_root = Path(__file__).parent res = subprocess.run('git ls-files'.split(), check=True, capture_output=True, text=True) for path in res.stdout.splitlines(): - if re.fullmatch(r'de\.jaseg\.kimesh\.[^/]*-v[.0-9]*\.zip', path.strip()): + if re.fullmatch(r'de\.jaseg\.kicoil\.[^/]*-v[.0-9]*\.zip', path.strip()): print(f'Removing old release zip {path} from git index.') subprocess.run(['git', 'rm', path], check=True, capture_output=True) - for pkg_dir in Path('de.jaseg.kimesh.plugin'), Path('de.jaseg.kimesh.footprints'): - # NOTE: metadata.json appears twice. In what I believe is a sub-optimal design choice, the variant in the - # archive is only allowed to contain the current version in its version list without its zip file metadata, - # while the variant in the repository index is supposed to contain all past versions including their zip file - # metadata. AFAICT they are the same otherwise. - meta_path = Path(f'{pkg_dir}-repo-metadata.json') + plugin_sources = project_root / 'kicad-plugin' + pkg_dir = project_root / 'de.jaseg.kicoil' - print(f'Updating metadata file {meta_path}') - ver_dict = { - 'version': version, - 'status': 'stable', - 'kicad_version': '7.99', - } + if pkg_dir.is_dir(): + shutil.rmtree(pkg_dir) - # Include just the version metadata in the metadata for the archive - meta_file = json.loads(meta_path.read_text()) - meta_file['versions'] = [ver_dict] - (pkg_dir / 'metadata.json').write_text(json.dumps(meta_file, indent=4)) + shutil.copytree(plugin_sources, pkg_dir) + shutil.copy(project_root / 'LICENSE', pkg_dir) - zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.')) + meta_path = project_root / 'metadata.json' + print(f'Updating metadata file {meta_path}') + ver_dict = { + 'version': version, + 'status': 'stable', + 'kicad_version': '9.00', + } + + meta_file = json.loads(meta_path.read_text()) + meta_file['versions'] = [ver_dict] + (pkg_dir / 'metadata.json').write_text(json.dumps(meta_file, indent=4)) + + res = subprocess.run(['uv', 'export', '--no-hashes', '--format', 'requirements-txt'], + check=True, capture_output=True, text=True) + (pkg_dir / 'requirements.txt').write_text(res.stdout) + + shutil.copytree(project_root / 'src' / 'kicoil', pkg_dir / 'kicoil') + + zip_fn = Path(shutil.make_archive(f'{pkg_dir.name}-v{version}', 'zip', pkg_dir, '.')) + if not dry_run: print(f'Adding new release zip {zip_fn} to git index.') subprocess.run(['git', 'add', str(zip_fn)], check=True, capture_output=True) - # Add the zip's metadata to the metadata for the repository - ver_dict['download_sha256'] = hashlib.sha256(zip_fn.read_bytes()).hexdigest() - ver_dict['download_size'] = zip_fn.stat().st_size - ver_dict['download_url'] = f'https://git.jaseg.de/kimesh.git/plain/{zip_fn.name}?h=v{version}' - ver_dict['install_size'] = tree_size(pkg_dir) + # Add the zip's metadata to the metadata for the repository + ver_dict['download_sha256'] = hashlib.sha256(zip_fn.read_bytes()).hexdigest() + ver_dict['download_size'] = zip_fn.stat().st_size + ver_dict['download_url'] = f'https://git.jaseg.de/kimesh.git/plain/{zip_fn.name}?h=v{version}' + ver_dict['install_size'] = tree_size(pkg_dir) - meta_file = json.loads(meta_path.read_text()) - meta_file['versions'].append(ver_dict) - meta_path.write_text(json.dumps(meta_file, indent=4)) + meta_file = json.loads(meta_path.read_text()) + meta_file['versions'].append(ver_dict) + meta_path.write_text(json.dumps(meta_file, indent=4)) + if not dry_run: print(f'Adding updated metadata file {meta_path} to git index') subprocess.run(['git', 'add', str(meta_path)], check=True, capture_output=True) - print('Create git commit') - subprocess.run(['git', 'commit', '-m', f'Version {version}', '--no-edit'], check=True, capture_output=True) - res = subprocess.run('git rev-parse --short HEAD'.split(), check=True, capture_output=True, text=True) - print(f'Created commit {res.stdout.strip()}') - print(f'Creating and signing version tag v{version}') - subprocess.run(['git', - '-c', 'user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D', - '-c', 'user.email=python-mpv@jaseg.de', - 'tag', '-s', f'v{version}', '-m', f'Version v{version}'], - check=True) + if not dry_run: + print('Create git commit') + subprocess.run(['git', 'commit', '-m', f'Version {version}', '--no-edit'], check=True, capture_output=True) + res = subprocess.run('git rev-parse --short HEAD'.split(), check=True, capture_output=True, text=True) + print(f'Created commit {res.stdout.strip()}') + print(f'Creating and signing version tag v{version}') + subprocess.run(['git', + '-c', 'user.signingkey=E36F75307F0A0EC2D145FF5CED7A208EEEC76F2D', + '-c', 'user.email=python-mpv@jaseg.de', + 'tag', '-s', f'v{version}', '-m', f'Version v{version}'], + check=True) if __name__ == '__main__': do_release() diff --git a/pyproject.toml b/pyproject.toml index c212c0f..00d4825 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,11 @@ description = "Planar Inductor Generator" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.13" -dependencies = ["click", "gerbonara>=1.6.0"] +dependencies = [ + "click", + "gerbonara>=1.6.0", + "kicad-python>=0.5.0", +] authors = [{ name = "jaseg" }] maintainers = [ { name = "Kicoil maintainers", email = "kicoil@jaseg.de" }, diff --git a/src/kicoil/gui.py b/src/kicoil/gui.py index 3f0aaa8..c307062 100644 --- a/src/kicoil/gui.py +++ b/src/kicoil/gui.py @@ -122,7 +122,8 @@ class EntryWithPlaceholder(tk.Entry): class KiCoilGUI: - def __init__(self, root): + def __init__(self, root, kicad_inst=None): + self.kicad_inst = kicad_inst self.root = root self.root.title("KiCoil - Planar Inductor Generator") self.root.geometry("1000x650") @@ -175,8 +176,11 @@ class KiCoilGUI: ttk.Button(button_frame, text="Show Valid Twists", command=self.show_valid_twists, width=20).pack(side=tk.LEFT, padx=5) - ttk.Button(button_frame, text="Generate and Save", - command=self.generate_footprint, width=20).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="Save Footprint File", + command=self.save_footprint_file, width=20).pack(side=tk.LEFT, padx=5) + if self.kicad_inst: + ttk.Button(button_frame, text="Update Footprint on Board", + command=self.update_board_footprint, width=20).pack(side=tk.LEFT, padx=5) status_label = ttk.Label(main_frame, text="Output:", font=('Helvetica', 10, 'bold')) status_label.grid(row=3, column=0, sticky=tk.W, pady=(10, 0)) @@ -496,10 +500,10 @@ class KiCoilGUI: def capture_warnings(self): """Context manager to capture kicoil's warnings to the output text widget""" def show_warning(message, category, filename, lineno, file=None, line=None): - self.output_text.config(state='normal') + self.output_text['state'] = 'normal' self.output_text.insert(tk.END, f'{message}\n', 'warning') self.output_text.see(tk.END) - self.output_text.config(state='disabled') + self.output_text['state'] = 'disabled' self.output_text.update_idletasks() old_showwarning, warnings.showwarning = warnings.showwarning, show_warning @@ -540,7 +544,7 @@ class KiCoilGUI: def validate_parameters(self): """Validate parameters by creating PlanarInductor instance""" try: - self.output_text.config(state='normal') + self.output_text['state'] = 'normal' self.output_text.delete('1.0', tk.END) with self.capture_warnings(): @@ -572,7 +576,7 @@ class KiCoilGUI: return True finally: - self.output_text.config(state='disabled') + self.output_text['state'] = 'disabled' def update_placeholders(self): if self.current_model is None: @@ -622,20 +626,99 @@ class KiCoilGUI: def show_valid_twists(self): - """Show valid twist counts for current number of turns""" turns = self.turns_var.get() valid_twists = list(divisors(turns, turns)) - self.output_text.config(state='normal') + self.output_text['state'] = 'normal' self.output_text.delete('1.0', tk.END) self.output_text.insert('1.0', f'Valid twist counts for {turns} turns:\n') for d in valid_twists: self.output_text.insert(tk.END, f' {d}\n') - self.output_text.config(state='disabled') + self.output_text['state'] = 'disabled' - def generate_footprint(self): - """Generate the KiCad footprint using the validated model""" + def update_board_footprint(self): + if not self.validate_parameters(): + messagebox.showerror("Error", "Cannot generate model. Please check the output for warnings or errors.") + return + from kipy.board_types import FootprintInstance, Footprint, Pad, BoardArc, BoardSegment, FootprintAttributes,\ + PadStack, PadStackLayer, PadStackType, PadStackShape, DrillProperties, PadType + from kipy.geometry import Vector2 + from kipy.common_types import GraphicAttributes, StrokeAttributes + from kipy.util import from_mm + from kipy.util.board_layer import CANONICAL_LAYER_NAMES + + from gerbonara.cad.kicad.footprints import Atom + + board = self.kicad_inst.get_board() + selected = [item for item in board.get_selection() if isinstance(item, FootprintInstance)] + + if not selected: + messagebox.showerror("Error", "No footprint selected. Select one footprint to replace in KiCad's PCB editor.") + return + elif len(selected) > 1: + messagebox.showerror("Error", "More than one footprint selected. Select only the footprint you want to replace.") + return + + selected_footprint, = selected + + footprint_name = self.footprint_name_entry.get() or None + arc_tolerance = self.arc_tolerance_var.get() + circle_segments = self.circle_segments_var.get() + + self.output_text['state'] = 'normal' + self.output_text.insert(tk.END, "Rendering footprint...\n", 'info') + self.output_text.see(tk.END) + + model = self.current_model.render_footprint(footprint_name, arc_tolerance, circle_segments) + selected_footprint.attributes.exclude_from_bill_of_materials = True + layer_map = {v: k for k, v in CANONICAL_LAYER_NAMES.items()} + items = [] + + for line in model.lines: + seg = BoardSegment() + seg.start = Vector2.from_xy(from_mm(line.start.x), from_mm(line.start.y)) + seg.end = Vector2.from_xy(from_mm(line.end.x), from_mm(line.end.y)) + seg.attributes.stroke.width = from_mm(line.stroke.width) + seg.layer = layer_map[line.layer] + selected_footprint.definition.add_item(seg) + items.append(seg) + + for ref in model.arcs: + arc = BoardArc() + arc.start = Vector2.from_xy(from_mm(ref.start.x), from_mm(ref.start.y)) + arc.mid = Vector2.from_xy(from_mm(ref.mid.x), from_mm(ref.mid.y)) + arc.end = Vector2.from_xy(from_mm(ref.end.x), from_mm(ref.end.y)) + arc.attributes.stroke.width = from_mm(ref.stroke.width) + arc.layer = layer_map[ref.layer] + selected_footprint.definition.add_item(arc) + items.append(arc) + + for ref in model.pads: + pad = Pad() + pad.number = ref.number + pad.position = Vector2.from_xy(from_mm(ref.at.x), from_mm(ref.at.y)) + pad.type = PadType.PT_SMD if ref.type == Atom.smd else PadType.PT_PTH + pad.padstack.type = PadStackType.PST_NORMAL + pad.padstack.layers = [layer_map[name] for name in ref.layers] + layer = pad.padstack.copper_layers[0] + layer.shape = PadStackShape.PSS_CIRCLE + layer.size = Vector2.from_xy(from_mm(ref.size.x), from_mm(ref.size.y)) + layer.layer = layer_map[ref.layers[0]] # ? duplicate + if ref.drill: + pad.padstack.drill.diameter = Vector2.from_xy(from_mm(ref.drill.diameter), from_mm(ref.drill.diameter)) + selected_footprint.definition.add_item(pad) + items.append(pad) + + commit = board.begin_commit() + board.create_items(items) + board.update_items([selected_footprint]) + board.push_commit(commit, 'Updated planar coil footprint') + self.output_text.insert(tk.END, "Done.", 'info') + self.output_text['state'] = 'disabled' + self.output_text.see(tk.END) + + def save_footprint_file(self): if not self.validate_parameters(): messagebox.showerror("Error", "Cannot generate model. Please check the output for warnings or errors.") return @@ -645,7 +728,7 @@ class KiCoilGUI: arc_tolerance = self.arc_tolerance_var.get() circle_segments = self.circle_segments_var.get() - self.output_text.config(state='normal') + self.output_text['state'] = 'normal' self.output_text.insert(tk.END, "Rendering footprint...\n\n", 'info') footprint = self.current_model.render_footprint(footprint_name, arc_tolerance, circle_segments) @@ -666,7 +749,7 @@ class KiCoilGUI: except Exception as e: tb = traceback.format_exc() - self.output_text.config(state='normal') + self.output_text['state'] = 'normal' self.output_text.insert(tk.END, f"\nError generating footprint:\n{tb}\n", 'error') self.output_text.see(tk.END) @@ -675,13 +758,13 @@ class KiCoilGUI: messagebox.showerror("Error", f"Error generating footprint: {e}") finally: - self.output_text.config(state='disabled') + self.output_text['state'] = 'disabled' -def main(): +def main(kicad_inst=None): + from kipy import KiCad + from kipy.errors import ConnectionError + kicad_inst = KiCad() root = tk.Tk() - app = KiCoilGUI(root) + app = KiCoilGUI(root, kicad_inst) root.mainloop() - -if __name__ == "__main__": - main() diff --git a/uv.lock b/uv.lock index a5837da..9afee10 100644 --- a/uv.lock +++ b/uv.lock @@ -243,6 +243,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "kicad-python" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "pynng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/ad/b33939d2e0c099f04875c250564dde79baec09927c5c70a7a101cd612d94/kicad_python-0.5.0.tar.gz", hash = "sha256:9abb6ad87b822b6c63c80543e485e674bddc46080f9706ae3dff7f05d8dc63f9", size = 198708, upload-time = "2025-10-13T15:10:03.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/ab/b9edb6088ba8263a7f2446f1f3ae088c50bde149d7dc149abca32c4d21d2/kicad_python-0.5.0-py3-none-any.whl", hash = "sha256:fe7adeff9f1c23a76f2ace9468faadcbeef915ce026b187ca35db1cf4faa4451", size = 130154, upload-time = "2025-10-13T15:10:02.695Z" }, +] + [[package]] name = "kicoil" version = "0.9.0" @@ -250,6 +263,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "gerbonara" }, + { name = "kicad-python" }, ] [package.dev-dependencies] @@ -262,6 +276,7 @@ gui = [ requires-dist = [ { name = "click" }, { name = "gerbonara", specifier = ">=1.6.0" }, + { name = "kicad-python", specifier = ">=0.5.0" }, ] [package.metadata.requires-dev] @@ -389,6 +404,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/5f/82c8074f7e84978129347c2c6ec8b6c59f3584ff1a20bc3c940a3e061790/priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa", size = 8946, upload-time = "2021-06-27T10:15:03.856Z" }, ] +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -398,6 +427,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] +[[package]] +name = "pynng" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/8c/23141a4b94fdb69c72fe54734a5da192ecbaf5c4965ba6d3a753e6a8ac34/pynng-0.8.1.tar.gz", hash = "sha256:60165f34bdf501885e0acceaeed79bc35a57f3ca3c913cb38c14919b9bd3656f", size = 6364925, upload-time = "2025-01-16T03:42:32.848Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/fd/b6b43259bf87c7640824310c930761ea814eb4b726f2814ef847ad80d96d/pynng-0.8.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1013dc1773e8a4cee633a8516977d59c17711b56b0df9d6c174d8ac722b19d9", size = 1089913, upload-time = "2025-01-16T03:41:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/44/71/134faf3a6689898167c0b8a55b8a55069521bc79ae6eed1657b075545481/pynng-0.8.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a89b5d3f9801913a22c85cf320efdffc1a2eda925939a0e1a6edc0e194eab27", size = 727669, upload-time = "2025-01-16T03:41:04.936Z" }, + { url = "https://files.pythonhosted.org/packages/72/3d/2d77349fa87671d31c5c57ea44365311338b0a8d984e8b095add62f18fda/pynng-0.8.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2f0a7fdd96c99eaf1a1fce755a6eb39e0ca1cf46cf81c01abe593adabc53b45", size = 938072, upload-time = "2025-01-16T03:41:09.685Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/580382d32fe90dd3cc0310358d449991070091b78a8f97df3f8e4b3d5fee/pynng-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cbda575215e854a241ae837aac613e88d197b0489ef61f4a42f2e9dd793f01", size = 736250, upload-time = "2025-01-16T03:41:11.304Z" }, + { url = "https://files.pythonhosted.org/packages/b6/db/9bf6a8158187aa344c306c6037ff20d134132d83596dcbb8537faaad610d/pynng-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f635d6361f9ad81d16ba794a5a9b3aa47ed92a7709b88396523676cb6bddb1f", size = 941514, upload-time = "2025-01-16T03:41:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/26/f3/9a7676e3d115834a5acf674590bb32e61f9caa5b8f0971628fc562670d35/pynng-0.8.1-cp313-cp313-win32.whl", hash = "sha256:6d5c51249ca221f0c4e27b13269a230b19fc5e10a60cbfa7a8109995b22e861e", size = 370575, upload-time = "2025-01-16T03:41:15.998Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/11d76f7aeb733e966024eb6b6adf73280d2c600f8fa8bdb6ea34d33e9a19/pynng-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f9c52bca0d063843178d6f43a302e0e2d6fbe20272de5b3c37f4873c3d55a42", size = 450453, upload-time = "2025-01-16T03:41:17.604Z" }, +] + [[package]] name = "quart" version = "0.20.0" @@ -434,6 +482,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/50/0a9e7e7afe7339bd5e36911f0ceb15fed51945836ed803ae5afd661057fd/rtree-1.4.1-py3-none-win_arm64.whl", hash = "sha256:3d46f55729b28138e897ffef32f7ce93ac335cb67f9120125ad3742a220800f0", size = 355253, upload-time = "2025-08-13T19:32:00.296Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + [[package]] name = "tinycss2" version = "1.5.1"