diff --git a/pyproject.toml b/pyproject.toml index 453bb7f..499d209 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Planar Inductor Generator" readme = "README.rst" license = "Apache-2.0" requires-python = ">=3.13" -dependencies = ["click", "gerbonara"] +dependencies = ["click", "gerbonara>=1.6.0"] authors = [{ name = "jaseg" }] maintainers = [ { name = "Kicoil maintainers", email = "kicoil@jaseg.de" }, diff --git a/src/kicoil/__init__.py b/src/kicoil/__init__.py new file mode 100644 index 0000000..bc23f58 --- /dev/null +++ b/src/kicoil/__init__.py @@ -0,0 +1,6 @@ + +from importlib.metadata import version + +from .geometry import PlanarInductor + +__version__ = version('kicoil') diff --git a/src/kicoil/cli.py b/src/kicoil/cli.py new file mode 100644 index 0000000..d9dd55f --- /dev/null +++ b/src/kicoil/cli.py @@ -0,0 +1,130 @@ + +import logging +import subprocess +import webbrowser +import tempfile +import os +import sys +from pathlib import Path +import warnings + +import click +from gerbonara.layers import LayerStack + +from .geometry import PlanarInductor +from .kicad import footprint_to_board +from .svg import make_transparent_svg + + +def print_valid_twists(ctx, param, value): + if not value or ctx.resilient_parsing: + return + + print(f'Valid twist counts for {value} turns:', file=sys.stderr) + for d in divisors(value, value): + print(f' {d}', file=sys.stderr) + + click.echo() + ctx.exit() + + +@click.command() +@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path)) +@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.") +@click.option('--layer-pair', default='F.Cu,B.Cu', help="Target KiCad layer pair for the generated footprint, comma-separated. Default: F.Cu/B.Cu.") +@click.option('--turns', type=int, default=5, help='Number of turns') +@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]') +@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]') +@click.option('--stagger-inner-vias/--no-stagger-inner-vias', default=False, help='Stagger inner via ring') +@click.option('--stagger-outer-vias/--no-stagger-outer-vias', default=False, help='Stagger outer via ring') +@click.option('--trace-width', type=float, default=None) +@click.option('--via-diameter', type=float, default=0.6) +@click.option('--via-drill', type=float, default=None) +@click.option('--via-offset', type=float, default=None, help='Radially offset vias from trace endpoints [mm]') +@click.option('--keepout-zone/--no-keepout-zone', default=True, help='Add a keepout are to the footprint (default: yes)') +@click.option('--keepout-margin', type=float, default=5, help='Margin between outside of coil and keepout area (mm, default: 5)') +@click.option('--copper-thickness', type=float, default=0.035, help='Copper thickness for resistance calculation, in mm. Default: 0.035mm ^= 1 Oz') +@click.option('--twists', type=int, default=1, help='Number of twists per revolution. Note that this number must be co-prime to the number of turns. Run with --show-twists to list valid values. (default: 1)') +@click.option('--circle-segments', type=int, default=64, help='When not using arcs, the number of points to use for arc interpolation per 360 degrees.') +@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.') +@click.option('--clearance', type=float, default=None) +@click.option('--arc-tolerance', type=float, default=0.02) +@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'show']), default='kicad-footprint') +@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)') +@click.option('--clockwise/--counter-clockwise', help='Direction of generated spiral. Default: counter-clockwise when wound from the inside.') +@click.option('--single-layer/--two-layer', help='Single-layer mode. This just forces twists to 0.') +@click.version_option() +def cli(outfile, footprint_name, clipboard, single_layer, arc_tolerance, circle_segments, format, **kwargs): + logger = logging.getLogger('kicoil') + + if single_layer: + kwargs['twists'] = 0 + kwargs['layers'] = 1 + else: + kwargs['layers'] = 2 + + try: + model = PlanarInductor(**kwargs) + + if footprint_name is None and outfile: + footprint_name = outfile.stem + + footprint = model.render_footprint(footprint_name, arc_tolerance, circle_segments) + + except ValueError as e: + raise click.ClickException(*e.args) + + data = None + if format == 'kicad-footprint': + data = footprint.serialize() + + elif format == 'kicad-pcb': + data = footprint_to_board(footprint).serialize() + + elif format == 'gerber': + stack = LayerStack() + footprint.render(stack) + + if not clipboard and outfile and outfile.suffix.lower() != '.zip': + stack.save_to_directory(outfile) + return + + else: + with tempfile.NamedTemporaryFile(delete_on_close=False) as f: + f = Path(f.name) + stack.save_to_zipfile(f) + data = f.read_bytes() + + elif format in ('svg', 'show'): + data = str(make_transparent_svg(footprint)) + + if format == 'show': + with tempfile.NamedTemporaryFile('w', suffix='.svg', delete=False) as f: + f.write(data) + f.flush() + webbrowser.open_new_tab(f'file://{f.name}') + return + + if clipboard: + if 'WAYLAND_DISPLAY' in os.environ: + copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip' + else: + copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard' + + try: + logger.info(f'Running {copy[0]}.', file=sys.stderr) + proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=isinstance(data, str)) + proc.communicate(data) + + except FileNotFoundError: + raise click.ClickException(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr) + + elif not outfile: + if isinstance(data, str): + print(data) + else: + sys.stdout.buffer.write(data) + + else: + outfile.write_text(data) + diff --git a/src/kicoil/geometry.py b/src/kicoil/geometry.py index c222aa7..d691dff 100644 --- a/src/kicoil/geometry.py +++ b/src/kicoil/geometry.py @@ -1,34 +1,16 @@ -#!/usr/bin/env python3 - import warnings -import subprocess +import logging +from dataclasses import dataclass, field import sys -import math -import multiprocessing -import os from math import * -from pathlib import Path -from itertools import cycle -from contextlib import contextmanager -from scipy.constants import mu_0 -import numpy as np -import click -import matplotlib as mpl +from gerbonara.cad.kicad.footprints import Footprint +from gerbonara.cad.kicad.primitives import Zone, Hatch, ZoneKeepout, ZonePolygon, XYCoord -from gerbonara.cad.kicad import pcb as kicad_pcb -from gerbonara.cad.kicad import footprints as kicad_fp -from gerbonara.cad.kicad import graphical_primitives as kicad_gr -from gerbonara.cad.kicad import primitives as kicad_pr -from gerbonara.utils import Tag -from gerbonara import graphic_primitives as gp -from gerbonara import graphic_objects as go - -from . import svg from . import kicad -__version__ = '1.0.0' +mu_0 = 1.25663706127e-06 # from scipy.constants def point_line_distance(p, l1, l2): @@ -64,13 +46,11 @@ def farey_sequence(n: int, descending: bool = False) -> None: a, b, c, d = 0, 1, 1, n if descending: a, c = 1, n - 1 - #print(f"{a}/{b}") yield a, b while c <= n and not descending or a > 0 and descending: k = (n + b) // d a, b, c, d = c, d, k * c - a, k * d - b - #print(f"{a}/{b}") yield a, b @@ -82,19 +62,7 @@ def divisors(n, max_b=10): yield a -def print_valid_twists(ctx, param, value): - if not value or ctx.resilient_parsing: - return - - print(f'Valid twist counts for {value} turns:', file=sys.stderr) - for d in divisors(value, value): - print(f' {d}', file=sys.stderr) - - click.echo() - ctx.exit() - - -def arc_approximate(points, layer, tolerance=0.02, level=0): +def arc_approximate(points, trace_width, layer, tolerance=0.02, level=0): """ Approximate spiral arm using circular arcs. This results in a smoother output using less segments than if we approximate the arc using straight line segments. @@ -111,7 +79,7 @@ def arc_approximate(points, layer, tolerance=0.02, level=0): x2, y2 = points[-1] if len(points) < 5: - yield make_arc(x0, y0, x2, y2, x1, y1, layer) + yield kicad.make_arc(x0, y0, x2, y2, x1, y1, trace_width, layer) # https://stackoverflow.com/questions/56224824/how-do-i-find-the-circumcenter-of-the-triangle-using-python-without-external-lib d = 2 * (x0 * (y2 - y1) + x2 * (y1 - y0) + x1 * (y0 - y2)) @@ -119,14 +87,14 @@ def arc_approximate(points, layer, tolerance=0.02, level=0): cy = ((x0 * x0 + y0 * y0) * (x1 - x2) + (x2 * x2 + y2 * y2) * (x0 - x1) + (x1 * x1 + y1 * y1) * (x2 - x0)) / d r = dist((cx, cy), (x1, y1)) if any(abs(dist((px, py), (cx, cy)) - r) > tolerance for px, py in points): - yield from arc_approximate(points[:i_mid+1], layer, tolerance, level+1) - yield from arc_approximate(points[i_mid:], layer, tolerance, level+1) + yield from arc_approximate(points[:i_mid+1], trace_width, layer, tolerance, level+1) + yield from arc_approximate(points[i_mid:], trace_width, layer, tolerance, level+1) else: - yield make_arc(x0, y0, x2, y2, x1, y1, layer) + yield kicad.make_arc(x0, y0, x2, y2, x1, y1, trace_width, layer) -def compute_spiral(r1, r2, a1, a2, start_frac, end_frac, fn=64): +def compute_spiral(r1, r2, a1, a2, fn=64): fn = ceil(fn * (a2-a1)/(2*pi)) x0, y0 = cos(a1)*r1, sin(a1)*r1 dr = 3 if r2 < r1 else -3 @@ -144,250 +112,242 @@ def compute_spiral(r1, r2, a1, a2, start_frac, end_frac, fn=64): return points, sum(dists) -@click.command() -@click.argument('outfile', required=False, type=click.Path(writable=True, dir_okay=False, path_type=Path)) -@click.option('--footprint-name', help="Name for the generated footprint. Default: Output file name sans extension.") -@click.option('--layer-pair', default='F.Cu,B.Cu', help="Target KiCad layer pair for the generated footprint, comma-separated. Default: F.Cu/B.Cu.") -@click.option('--turns', type=int, default=5, help='Number of turns') -@click.option('--outer-diameter', type=float, default=50, help='Outer diameter [mm]') -@click.option('--inner-diameter', type=float, default=25, help='Inner diameter [mm]') -@click.option('--stagger-inner-vias/--no-stagger-inner-vias', default=False, help='Stagger inner via ring') -@click.option('--stagger-outer-vias/--no-stagger-outer-vias', default=False, help='Stagger outer via ring') -@click.option('--trace-width', type=float, default=None) -@click.option('--via-diameter', type=float, default=0.6) -@click.option('--two-layer/--single-layer', default=True) -@click.option('--via-drill', type=float, default=0.3) -@click.option('--via-offset', type=float, default=None, help='Radially offset vias from trace endpoints [mm]') -@click.option('--keepout-zone/--no-keepout-zone', default=True, help='Add a keepout are to the footprint (default: yes)') -@click.option('--keepout-margin', type=float, default=5, help='Margin between outside of coil and keepout area (mm, default: 5)') -@click.option('--copper-thickness', type=float, default=0.035, help='Copper thickness for resistance calculation, in mm. Default: 0.035mm ^= 1 Oz') -@click.option('--twists', type=int, default=1, help='Number of twists per revolution. Note that this number must be co-prime to the number of turns. Run with --show-twists to list valid values. (default: 1)') -@click.option('--circle-segments', type=int, default=64, help='When not using arcs, the number of points to use for arc interpolation per 360 degrees.') -@click.option('--show-twists', callback=print_valid_twists, expose_value=False, type=int, is_eager=True, help='Calculate and show valid --twists counts for the given number of turns. Takes the number of turns as a value.') -@click.option('--clearance', type=float, default=None) -@click.option('--arc-tolerance', type=float, default=0.02) -@click.option('--format', type=click.Choice(['svg', 'gerber', 'kicad-footprint', 'kicad-pcb', 'show']), default='kicad-footprint') -@click.option('--clipboard/--no-clipboard', help='Use clipboard integration (requires wl-clipboard)') -@click.option('--counter-clockwise/--clockwise', help='Direction of generated spiral. Default: clockwise when wound from the inside.') -@click.version_option() -def generate(outfile, turns, outer_diameter, inner_diameter, via_diameter, via_drill, via_offset, trace_width, - clearance, footprint_name, layer_pair, twists, clipboard, counter_clockwise, keepout_zone, keepout_margin, - arc_tolerance, circle_segments, copper_thickness, format, two_layer, stagger_inner_vias, - stagger_outer_vias): - if 'WAYLAND_DISPLAY' in os.environ: - copy, paste, cliputil = ['wl-copy'], ['wl-paste'], 'xclip' - else: - copy, paste, cliputil = ['xclip', '-i', '-sel', 'clipboard'], ['xclip', '-o', '-sel' 'clipboard'], 'wl-clipboard' +@dataclass +class PlanarInductor(): + outer_diameter: float + inner_diameter: float + turns: int + twists: int + trace_width: float + clearance: float = None + layers: int = 2 + via_diameter: float = 0.6 + via_drill: float = None + via_offset: float = None + stagger_inner_vias: bool = False + stagger_outer_vias: bool = False + keepout_zone: bool = True + keepout_margin: float = 0.0 + copper_thickness: float = 0.035 + layer_pair: str = 'F.Cu,B.Cu' + clockwise: bool = False - if gcd(twists, turns) != 1: - raise click.ClickException(f'For the geometry to work out, the --twists parameter must be co-prime to --turns, i.e. the two must have 1 as their greatest common divisor. You can print valid values for --twists by running this command with --show-twists [turns number].\n\n' - f'Right now, both are divisible by {gcd(twists, turns)}.\n' - f'Valid twist counts for n={turns} turns are: {list(divisors(turns, max(turns, 25)))}' - f'Valid turn counts for k={twists} twists are: {list(divisors(twists, max(twists, 25)))}') + def __post_init__(self): + self.logger = logging.getLogger('kicoil') + self.outer_radius = self.outer_diameter/2 + self.inner_radius = self.inner_diameter/2 + self.turns_per_layer = self.turns/self.layers + self.sector_angle = 2*pi / self.twists + self.sweeping_angle = self.sector_angle * self.turns_per_layer + self.spiral_pitch = (self.outer_radius-self.inner_radius) / self.turns_per_layer + self.layers = 2 if self.twists > 0 else 1 + self.R = None # will be calculated during render - if (stagger_inner_vias or stagger_outer_vias) and twists%2 != 0: - raise click.ClickException('For --stagger-inner/outer-vias to work, --twists must be even and --turns must be odd.') + c1 = self.inner_radius + c2 = self.inner_radius + self.spiral_pitch + alpha1 = atan((self.outer_radius - self.inner_radius) / self.sweeping_angle / c1) + alpha2 = atan((self.outer_radius - self.inner_radius) / self.sweeping_angle / c2) + alpha = (alpha1+alpha2)/2 + self.projected_spiral_pitch = self.spiral_pitch*cos(alpha) - outer_radius = outer_diameter/2 - inner_radius = inner_diameter/2 - turns_per_layer = turns/2 if two_layer else turns - sweeping_angle = 2*pi * turns_per_layer / twists - spiral_pitch = (outer_radius-inner_radius) / turns_per_layer - c1 = inner_radius - c2 = inner_radius + spiral_pitch - alpha1 = atan((outer_radius - inner_radius) / sweeping_angle / c1) - alpha2 = atan((outer_radius - inner_radius) / sweeping_angle / c2) - alpha = (alpha1+alpha2)/2 - projected_spiral_pitch = spiral_pitch*cos(alpha) + if self.turns < 1: + raise ValueError(f'Error: PlanarInductor.turns must be 1 or more') - if trace_width is None and clearance is None: - trace_width = 0.15 - print(f'Warning: Defaulting to {trace_width:.2f} mm trace width.', file=sys.stderr) + if self.twists < 0: + raise ValueError(f'Error: PlanarInductor.turns must be 0 or more') - if trace_width is None: - if round(clearance, 3) > round(projected_spiral_pitch, 3): - raise click.ClickException(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') - trace_width = projected_spiral_pitch - clearance - print(f'Calculated trace width for {clearance:.2f} mm clearance is {trace_width:.2f} mm.', file=sys.stderr) + if gcd(self.twists, self.turns) != 1: + raise ValueError(f'For the geometry to work out, the twists parameter must be co-prime to turns, i.e. the two must have 1 as their greatest common divisor. You can print valid values for twists by running the kicoil CLI with --show-twists [turns number].\n\n' + f'Right now, both are divisible by {gcd(self.twists, self.turns)}.\n' + f'Valid twist counts for n={self.turns} turns are: {list(divisors(self.turns, max(self.turns, 25)))}' + f'Valid turn counts for k={self.twists} twists are: {list(divisors(self.twists, max(self.twists, 25)))}') - elif clearance is None: - if round(trace_width, 2) > round(projected_spiral_pitch, 2): - raise click.ClickException(f'Error: Given trace width of {trace_width:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') - clearance = projected_spiral_pitch - trace_width - print(f'Calculated clearance for {trace_width:.2f} mm trace width is {clearance:.2f} mm.', file=sys.stderr) + if (self.stagger_inner_vias or self.stagger_outer_vias) and self.twists%2 != 0: + raise ValueError('For via staggering to work, twists must be even and turns must be odd.') - else: - if round(trace_width, 2) > round(projected_spiral_pitch, 2): - raise click.ClickException(f'Error: Given trace width of {trace_width:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') - clearance_actual = projected_spiral_pitch - trace_width - if round(clearance_actual, 3) < round(clearance, 3): - raise click.ClickException(f'Error: Actual clearance for {trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {clearance:.2f} mm.') + if self.trace_width is None and self.clearance is None: + self.clearance = 0.15 + warnings.warn(f'Warning: Neither trace width nor clearance given. Defaulting to {self.clearance:.2f} mm clearance.') - if round(via_diameter, 2) < round(trace_width, 2): - print(f'Clipping via diameter from {via_diameter:.2f} mm to trace width of {trace_width:.2f} mm.', file=sys.stderr) - via_diameter = trace_width + if self.trace_width is None: + if round(self.clearance, 3) > round(self.projected_spiral_pitch, 3): + raise ValueError(f'Error: Given clearance of {clearance:.2f} mm is larger than the projected spiral pitch of {projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') + self.trace_width = self.projected_spiral_pitch - self.clearance + self.logger.info(f'Calculated trace width for {self.clearance:.2f} mm clearance is {self.trace_width:.2f} mm.') - if via_offset is None: - via_offset = max(0, (via_diameter-trace_width)/2) - print(f'Autocalculated via offset {via_offset:.2f} mm', file=sys.stderr) - - inner_via_ring_radius = inner_radius - via_offset - #print(f'{inner_radius=} {via_offset=} {via_diameter=}', file=sys.stderr) - inner_via_angle = 2*asin((via_diameter + clearance)/2 / inner_via_ring_radius) - - outer_via_ring_radius = outer_radius + via_offset - outer_via_angle = 2*asin((via_diameter + clearance)/2 / outer_via_ring_radius) - - print(f'Inner via ring @r={inner_via_ring_radius:.2f} mm (from {inner_radius:.2f} mm)', file=sys.stderr) - print(f' {degrees(inner_via_angle):.1f} deg / via', file=sys.stderr) - print(f'Outer via ring @r={outer_via_ring_radius:.2f} mm (from {outer_radius:.2f} mm)', file=sys.stderr) - print(f' {degrees(outer_via_angle):.1f} deg / via', file=sys.stderr) - - # Check if the vias of the inner ring are so large that they would overlap - if inner_via_angle*twists > (4*pi if stagger_inner_vias else 2*pi): - min_dia = 2*((via_diameter + clearance) / (2*sin(pi / twists * (2 if stagger_inner_vias else 1))) + via_offset) - warnings.warn(f'Overlapping vias in inner via ring. Calculated minimum inner diameter is {min_dia:.2f} mm.') - - pitch = clearance + trace_width - t, _, b = layer_pair.partition(',') - layer_pair = (t.strip(), b.strip()) - rainbow = '#817 #a35 #c66 #e94 #ed0 #9d5 #4d8 #2cb #0bc #09c #36b #639'.split() - rainbow = rainbow[2::3] + rainbow[1::3] + rainbow[0::3] - n = 5 - rainbow = rainbow[n:] + rainbow[:n] - out_paths = [] - svg_stuff = [*out_paths] - - # For fill factor & inductance formulas, See https://coil32.net/pcb-coil.html for details - d_avg = (outer_diameter + inner_diameter)/2 - phi = (outer_diameter - inner_diameter) / (outer_diameter + inner_diameter) - c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20 - L = mu_0 * turns**2 * d_avg*1e3 * c1 / 2 * (log(c2/phi) + c3*phi + c4*phi**2) - print(f'Outer diameter: {outer_diameter:g} mm', file=sys.stderr) - print(f'Average diameter: {d_avg:g} mm', file=sys.stderr) - print(f'Inner diameter: {inner_diameter:g} mm', file=sys.stderr) - print(f'Fill factor: {phi:g}', file=sys.stderr) - print(f'Approximate inductance: {L:g} µH', file=sys.stderr) - - if footprint_name: - name = footprint_name - elif outfile: - name = outfile.stem, - else: - name = f'generated-coil-{outer_diameter:.2f}x{inner_diameter:.2f}-n{turns}-k{twists}' - - footprint = kicad_fp.Footprint( - name=name, - generator=kicad_fp.Atom('KicoilV1'), - layer='F.Cu', - descr=f"{turns} turn {outer_diameter:.2f} mm diameter twisted coil footprint, inductance approximately {L:.6f} µH. Generated by gerbonara'c Twisted Coil generator, version {__version__}.", - clearance=clearance, - zone_connect=0) - - sector_angle = 2*pi / twists - total_angle = twists*2*sweeping_angle if two_layer else twists*sweeping_angle - - inverse = {} - for i in range(twists): - inverse[i*turns%twists] = i - - # Array where we collect all gerbonara kicad line and arc objects - for i in range(twists): - start_angle = i*sector_angle - fold_angle = start_angle + sweeping_angle - end_angle = fold_angle + sweeping_angle - - # Handle the spiral arm - x = inverse[i]*floor(2*sweeping_angle / (2*pi)) * 2*pi - points_layer0, arm_length = compute_spiral(outer_radius, inner_radius, start_angle, fold_angle, (x + start_angle)/total_angle, (x + fold_angle)/total_angle, circle_segments) - x0, y0 = points_layer0[0] - xn, yn = points_layer0[-1] - - if two_layer: - # Handle the returning arm on the bottom layer - points_layer1, _ = compute_spiral(inner_radius, outer_radius, fold_angle, end_angle, (x + fold_angle)/total_angle, (x + end_angle)/total_angle, circle_segments) + elif self.clearance is None: + if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2): + raise ValueError(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') + self.clearance = self.projected_spiral_pitch - self.trace_width + self.logger.info(f'Calculated clearance for {self.trace_width:.2f} mm trace width is {self.clearance:.2f} mm.') else: - # Add a straight connecting segment connecting the inner point to the outside of the spiral. - dr = outer_radius - inner_radius - xq = xn + cos(fold_angle) * dr - yq = yn - sin(fold_angle) * dr - points_layer1 = [(xn, yn), (xq, yq)] + if round(self.trace_width, 2) > round(self.projected_spiral_pitch, 2): + raise click.ClickException(f'Error: Given trace width of {self.trace_width:.2f} mm is larger than the projected spiral pitch of {self.projected_spiral_pitch:.2f} mm. Reduce clearance or increase the size of the coil.') + clearance_actual = self.projected_spiral_pitch - self.trace_width + if round(clearance_actual, 3) < round(self.clearance, 3): + raise click.ClickException(f'Error: Actual clearance for {self.trace_width:.2f} mm trace is {clearance_actual:.2f} mm, which is lower than the given clearance of {self.clearance:.2f} mm.') - footprint.arcs.extend(arc_approximate(points_layer0, layer_pair[0], arc_tolerance)) - footprint.arcs.extend(arc_approximate(points_layer1, layer_pair[1], arc_tolerance)) + if round(self.via_diameter, 2) < round(self.trace_width, 2): + self.logger.warning(f'Clipping via diameter from {self.via_diameter:.2f} mm to trace width of {self.trace_width:.2f} mm.') + self.via_diameter = self.trace_width - # Handle inner via ring and process staggering if enabled - r = inner_via_ring_radius - if stagger_inner_vias: - if i%2 != 0: - r -= 2*via_offset + if self.via_drill is None: + self.via_drill = max(self.via_diameter / 2, self.via_diameter - 1.2) + self.logger.warning(f'No via drill given, defaulting to {self.via_drill:.2f} mm based on via diameter.') - xv, yv = r*cos(fold_angle), r*sin(fold_angle) + if self.via_offset is None: + self.via_offset = max(0, (self.via_diameter - self.trace_width)/2) + self.logger.info(f'Autocalculated via offset {self.via_offset:.2f} mm') - if not isclose(via_offset, 0, abs_tol=1e-6): - footprint.lines.append(make_line(*points_layer0[-1], xv, yv, layer_pair[0])) - footprint.lines.append(make_line(xv, yv, *points_layer1[0], layer_pair[1])) + if isclose(self.via_offset, 0, abs_tol=1e-6): + self.via_offset = 0 - # Handle outer via ring and process staggering if enabled unless we are at the start of the coil, where we will - # place pads below. - if i > 0: - r = outer_via_ring_radius + self.inner_via_ring_radius = self.inner_radius - self.via_offset + inner_via_angle = 2*asin((self.via_diameter + self.clearance)/2 / self.inner_via_ring_radius) - if stagger_outer_vias: + self.outer_via_ring_radius = self.outer_radius + self.via_offset + outer_via_angle = 2*asin((self.via_diameter + self.clearance)/2 / self.outer_via_ring_radius) + + self.logger.info(f'Inner via ring @r={self.inner_via_ring_radius:.2f} mm (from {self.inner_radius:.2f} mm)') + self.logger.info(f' {degrees(inner_via_angle):.1f} deg / via') + self.logger.info(f'Outer via ring @r={self.outer_via_ring_radius:.2f} mm (from {self.outer_radius:.2f} mm)') + self.logger.info(f' {degrees(outer_via_angle):.1f} deg / via') + + # Check if the vias of the inner ring are so large that they would overlap + if inner_via_angle*self.twists > (4*pi if self.stagger_inner_vias else 2*pi): + min_dia = 2*((self.via_diameter + self.clearance) / (2*sin(pi / self.twists * (2 if self.stagger_inner_vias else 1))) + self.via_offset) + warnings.warn(f'Overlapping vias in inner via ring. Calculated minimum inner diameter is {min_dia:.2f} mm.') + + t, _, b = self.layer_pair.partition(',') + self.layer_pair = (t.strip(), b.strip()) + + # For fill factor & inductance formulas, See https://coil32.net/pcb-coil.html for details + d_avg = (self.outer_diameter + self.inner_diameter)/2 + phi = (self.outer_diameter - self.inner_diameter) / (self.outer_diameter + self.inner_diameter) + c1, c2, c3, c4 = 1.00, 2.46, 0.00, 0.20 + self.L = mu_0 * self.turns**2 * d_avg*1e3 * c1 / 2 * (log(c2/phi) + c3*phi + c4*phi**2) + self.logger.info(f'Outer diameter: {self.outer_diameter:g} mm') + self.logger.info(f'Average diameter: {d_avg:g} mm') + self.logger.info(f'Inner diameter: {self.inner_diameter:g} mm') + self.logger.info(f'Fill factor: {phi:g}') + self.logger.info(f'Approximate inductance: {self.L:g} µH') + + _points, arm_length = compute_spiral(r1=self.outer_radius, r2=self.inner_radius, + a1=0, a2=self.sector_angle, + fn=256) + + self.track_length = arm_length*self.twists*self.layers + self.logger.info(f'Approximate track length: {self.track_length:.2f} mm') + + A = self.copper_thickness/1e3 * self.trace_width/1e3 # trace cross-section area + rho = 1.68e-8 # specific resistivity of copper + self.R = self.track_length/1e3 * rho / A + self.logger.info(f'Approximate resistance: {self.R:g} Ω') + + + def render_footprint(self, name=None, arc_tolerance=0.02, circle_segments=64): + if name is None: + name = f'generated-coil-{self.outer_diameter:.2f}x{self.inner_diameter:.2f}-n{self.turns}-k{self.twists}' + + from . import __version__ + footprint = Footprint( + name=name, + generator=kicad.Atom('kicoil'), + generator_version=__version__, + layer='F.Cu', + descr=f"{self.turns} turn {self.outer_diameter:.2f} mm diameter twisted coil footprint, inductance approximately {self.L:.6f} µH. Generated by kicoil, version {__version__}.", + clearance=self.clearance, + zone_connect=0) + + total_angle = self.twists*2*self.sweeping_angle*self.layers + + inverse = {} + for i in range(self.twists): + inverse[i*self.turns%self.twists] = i + + # Array where we collect all gerbonara kicad line and arc objects + for i in range(self.twists): + start_angle = i*self.sector_angle + fold_angle = start_angle + self.sweeping_angle + end_angle = fold_angle + self.sweeping_angle + + # Handle the spiral arm + x = inverse[i]*floor(2*self.sweeping_angle / (2*pi)) * 2*pi + points_layer0, arm_length = compute_spiral(r1=self.outer_radius, r2=self.inner_radius, + a1=start_angle, a2=fold_angle, + fn=circle_segments) + x0, y0 = points_layer0[0] + xn, yn = points_layer0[-1] + + if self.twists > 0: + # Handle the returning arm on the bottom layer + points_layer1, _ = compute_spiral(r1=self.inner_radius, r2=self.outer_radius, + a1=fold_angle, a2=end_angle, + fn=circle_segments) + + else: + # Add a straight connecting segment connecting the inner point to the outside of the spiral. + dr = self.outer_radius - self.inner_radius + xq = xn + cos(fold_angle) * dr + yq = yn - sin(fold_angle) * dr + points_layer1 = [(xn, yn), (xq, yq)] + + footprint.arcs.extend(arc_approximate(points_layer0, self.trace_width, self.layer_pair[0], arc_tolerance)) + footprint.arcs.extend(arc_approximate(points_layer1, self.trace_width, self.layer_pair[1], arc_tolerance)) + + # Handle inner via ring and process staggering if enabled + r = self.inner_via_ring_radius + if self.stagger_inner_vias: if i%2 != 0: - r += 2*via_offset + r -= 2*self.via_offset - xv, yv = r*cos(start_angle), r*sin(start_angle) + xv, yv = r*cos(fold_angle), r*sin(fold_angle) - if not isclose(via_offset, 0, abs_tol=1e-6): - footprint.lines.append(make_line(x0, y0, xv, yv, layer_pair[0])) - footprint.lines.append(make_line(x0, y0, xv, yv, layer_pair[1])) + if self.via_offset: + footprint.lines.append(kicad.make_line(*points_layer0[-1], xv, yv, self.trace_width, self.layer_pair[0])) + footprint.lines.append(kicad.make_line(xv, yv, *points_layer1[0], self.trace_width, self.layer_pair[1])) - l_total = arm_length*twists*(2 if two_layer else 1) - print(f'Approximate track length: {l_total:.2f} mm', file=sys.stderr) + footprint.pads.append(kicad.make_via(xv, yv, + self.via_diameter, self.via_drill, self.clearance, + self.layer_pair)) - A = copper_thickness/1e3 * trace_width/1e3 - rho = 1.68e-8 - R = l_total/1e3 * rho / A - print(f'Approximate resistance: {R:g} Ω', file=sys.stderr) + # Handle outer via ring and process staggering if enabled unless we are at the start of the coil, where we will + # place pads below. + if i > 0: + r = self.outer_via_ring_radius - # Place the pads on the outer radius - top_pad = make_pad(1, [layer_pair[0]], outer_radius, 0) - pads.append(top_pad) - bottom_pad = make_pad(2, [layer_pair[1]], outer_radius, 0) - pads.append(bottom_pad) + if self.stagger_outer_vias: + if i%2 != 0: + r += 2*self.via_offset - if keepout_zone: - r = outer_diameter/2 + keepout_margin - tol = 0.05 # mm - n = ceil(pi / acos(1 - tol/r)) - pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)] - footprint.zones.append(kicad_pr.Zone(layers=['*.Cu'], - hatch=kicad_pr.Hatch(), - filled_areas_thickness=False, - keepout=kicad_pr.ZoneKeepout(copperpour_allowed=False), - polygon=kicad_pr.ZonePolygon(pts=[kicad_pr.XYCoord(x=x, y=y) for x, y in pts]))) + xv, yv = r*cos(start_angle), r*sin(start_angle) - if format == 'kicad-footprint': - data = footprint.serialize() - elif format == 'kicad-pcb': - data = footprint_to_board(footprint).serialize() - elif format == 'gerber': + if self.via_offset: + footprint.lines.append(kicad.make_line(x0, y0, xv, yv, self.trace_width, self.layer_pair[0])) + footprint.lines.append(kicad.make_line(x0, y0, xv, yv, self.trace_width, self.layer_pair[1])) - if clipboard: - try: - print(f'Running {copy[0]}.', file=sys.stderr) - proc = subprocess.Popen(copy, stdin=subprocess.PIPE, text=True) - proc.communicate(data) - except FileNotFoundError: - print(f'Error: --clipboard requires the {copy[0]} and {paste[0]} utilities from {cliputil} to be installed.', file=sys.stderr) - elif not outfile: - print(data) - else: - outfile.write_text(data) + footprint.pads.append(kicad.make_via(xv, yv, + self.via_diameter, self.via_drill, self.clearance, + self.layer_pair)) + + # Place the pads on the outer radius + footprint.pads.extend([ + kicad.make_pad(1, [self.layer_pair[0]], self.outer_radius, 0, self.trace_width, self.clearance), + kicad.make_pad(2, [self.layer_pair[1]], self.outer_radius, 0, self.trace_width, self.clearance)]) + + if self.keepout_zone: + r = self.outer_diameter/2 + self.keepout_margin + tol = 0.05 # mm + n = ceil(pi / acos(1 - tol/r)) + pts = [(r*cos(a*2*pi/n), r*sin(a*2*pi/n)) for a in range(n)] + footprint.zones.append(Zone(layers=['*.Cu'], + hatch=Hatch(), + filled_areas_thickness=False, + keepout=ZoneKeepout(copperpour_allowed=False), + polygon=ZonePolygon(pts=[XYCoord(x=x, y=y) for x, y in pts]))) + + return footprint -if __name__ == '__main__': - generate() diff --git a/src/kicoil/kicad.py b/src/kicoil/kicad.py index 897026c..222a737 100644 --- a/src/kicoil/kicad.py +++ b/src/kicoil/kicad.py @@ -1,48 +1,49 @@ -from gerbonara.cad.kicad import footprints as kicad_fp +from gerbonara.cad.kicad.pcb import Board, TrackSegment, Via +from gerbonara.cad.kicad.footprints import Atom, AtPos, XYCoord, Pad, Line, Arc, Stroke, Drill -def make_pad(num, layer, x, y): - return kicad_fp.Pad( +def make_pad(num, layer, x, y, diameter, clearance): + return Pad( number=str(num), - type=kicad_fp.Atom.smd, - shape=kicad_fp.Atom.circle, - at=kicad_fp.AtPos(x=x, y=y), - size=kicad_fp.XYCoord(x=trace_width, y=trace_width), + type=Atom.smd, + shape=Atom.circle, + at=AtPos(x=x, y=y), + size=XYCoord(x=diameter, y=diameter), layers=layer, clearance=clearance, zone_connect=0) -def make_line(x1, y1, x2, y2, layer): - return kicad_fp.Line( - start=kicad_fp.XYCoord(x=x1, y=y1), - end=kicad_fp.XYCoord(x=x2, y=y2), +def make_line(x1, y1, x2, y2, trace_width, layer): + return Line( + start=XYCoord(x=x1, y=y1), + end=XYCoord(x=x2, y=y2), layer=layer, - stroke=kicad_fp.Stroke(width=trace_width)) + stroke=Stroke(width=trace_width)) -def make_arc(x1, y1, x2, y2, xm, ym, layer): - return kicad_fp.Arc( - start=kicad_fp.XYCoord(x=x1, y=y1), - mid=kicad_fp.XYCoord(x=xm, y=ym), - end=kicad_fp.XYCoord(x=x2, y=y2), +def make_arc(x1, y1, x2, y2, xm, ym, trace_width, layer): + return Arc( + start=XYCoord(x=x1, y=y1), + mid=XYCoord(x=xm, y=ym), + end=XYCoord(x=x2, y=y2), layer=layer, - stroke=kicad_fp.Stroke(width=trace_width)) + stroke=Stroke(width=trace_width)) -def make_via(x, y, layers): - return kicad_fp.Pad(number="NC", - type=kicad_fp.Atom.thru_hole, - shape=kicad_fp.Atom.circle, - at=kicad_fp.AtPos(x=x, y=y), - size=kicad_fp.XYCoord(x=via_diameter, y=via_diameter), - drill=kicad_fp.Drill(diameter=via_drill), +def make_via(x, y, diameter, drill, clearance, layers): + return Pad(number="NC", + type=Atom.thru_hole, + shape=Atom.circle, + at=AtPos(x=x, y=y), + size=XYCoord(x=diameter, y=diameter), + drill=Drill(diameter=drill), layers=layers, clearance=clearance, zone_connect=0) def footprint_to_board(footprint): - return kicad_pcb.Board.empty_board( + return Board.empty_board( zones=zones, - track_segments=[kicad_pcb.TrackSegment.from_footprint_line(line) for line in lines], - vias=[kicad_pcb.Via.from_pad(pad) for pad in pads if pad.type == kicad_pcb.Atom.thru_hole]) + track_segments=[TrackSegment.from_footprint_line(line) for line in lines], + vias=[Via.from_pad(pad) for pad in pads if pad.type == Atom.thru_hole]) diff --git a/src/kicoil/svg.py b/src/kicoil/svg.py index 028f705..0d54a63 100644 --- a/src/kicoil/svg.py +++ b/src/kicoil/svg.py @@ -1,84 +1,20 @@ -class SVGPath: - def __init__(self, **attrs): - self.d = '' - self.attrs = attrs +import textwrap - def line(self, x, y): - self.d += f'L {x} {y} ' +from gerbonara.utils import Tag +from gerbonara.layers import LayerStack - def move(self, x, y): - self.d += f'M {x} {y} ' +def make_transparent_svg(footprint): + stack = LayerStack() + footprint.render(stack) + root_tag = stack.to_svg(margin=5, colors={ + 'top copper': 'hsl(240 90% 65%)', + 'bottom copper': 'hsl( 0 90% 65%)', + 'drill pth': 'hsl(120 60% 50%)', + }) - def arc(self, x, y, r, large, sweep): - self.d += f'A {r} {r} 0 {int(large)} {int(sweep)} {x} {y} ' + root_tag.children[0].attrs['opacity'] = '98%' + root_tag.children[1].attrs['opacity'] = '98%' + root_tag.children[1].attrs['style'] = 'mix-blend-mode: multiply' + return root_tag - def close(self): - self.d += 'Z ' - - def __str__(self): - attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items()) - return f'' - -class SVGCircle: - def __init__(self, r, cx, cy, **attrs): - self.r = r - self.cx, self.cy = cx, cy - self.attrs = attrs - - def __str__(self): - attrs = ' '.join(f'{key.replace("_", "-")}="{value}"' for key, value in self.attrs.items()) - return f'' - -def svg_file(fn, stuff, vbw, vbh, vbx=0, vby=0): - with open(fn, 'w') as f: - f.write('\n') - f.write('\n') - f.write(f'>\n') - - for foo in stuff: - f.write(str(foo)) - - f.write('\n') - - -# This function was extruded using claude.ai. -def plasma_colormap(value): - """ - Calculate RGB color values according to matplotlib's plasma colormap. - - Args: - value: Float in range [0, 1] - - Returns: - tuple: (r, g, b) where each component is a float in range [0, 1] - """ - # Clamp value to [0, 1] - value = max(0.0, min(1.0, value)) - - # Key color points sampled from matplotlib's plasma colormap - # Format: (position, (r, g, b)) - colors = [ - (0.000, (0.050383, 0.029803, 0.527975)), - (0.125, (0.302735, 0.009615, 0.621789)), - (0.250, (0.489503, 0.011728, 0.656614)), - (0.375, (0.652325, 0.120106, 0.589517)), - (0.500, (0.789412, 0.275191, 0.472919)), - (0.625, (0.894832, 0.446214, 0.361309)), - (0.750, (0.965203, 0.627007, 0.262295)), - (0.875, (0.992373, 0.811467, 0.200941)), - (1.000, (0.940015, 0.975158, 0.131326)) - ] - - # Find the two color points to interpolate between - for i in range(len(colors) - 1): - pos1, color1 = colors[i] - pos2, color2 = colors[i + 1] - - if pos1 <= value <= pos2: - # Linear interpolation - t = (value - pos1) / (pos2 - pos1) - r = color1[0] + t * (color2[0] - color1[0]) - g = color1[1] + t * (color2[1] - color1[1]) - b = color1[2] + t * (color2[2] - color1[2]) - return (r, g, b) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a60d493 --- /dev/null +++ b/uv.lock @@ -0,0 +1,285 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flask" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, +] + +[[package]] +name = "gerbonara" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "quart" }, + { name = "rtree" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/d3/9ca276b8294e6aac53ab4f4c4d4f693187706e03bba55e9fbdbd7c06826a/gerbonara-1.6.0.tar.gz", hash = "sha256:f5b0016ea034fa6901182db3ed3699f58453b5130eb9518c775b135da4cc1e04", size = 1069607, upload-time = "2025-12-08T12:52:46.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/e2/a37dd288edc2584f83ed242a68a3c4ce5ea36cbda90064a38d4c6d472c6b/gerbonara-1.6.0-py3-none-any.whl", hash = "sha256:dde93c260b881ea4777f92ded320127976487b7c04fb3276130ae5193167cc6f", size = 1089543, upload-time = "2025-12-08T12:52:44.636Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "hypercorn" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "h2" }, + { name = "priority" }, + { name = "wsproto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/01/39f41a014b83dd5c795217362f2ca9071cf243e6a75bdcd6cd5b944658cc/hypercorn-0.18.0.tar.gz", hash = "sha256:d63267548939c46b0247dc8e5b45a9947590e35e64ee73a23c074aa3cf88e9da", size = 68420, upload-time = "2025-11-08T13:54:04.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/35/850277d1b17b206bd10874c8a9a3f52e059452fb49bb0d22cbb908f6038b/hypercorn-0.18.0-py3-none-any.whl", hash = "sha256:225e268f2c1c2f28f6d8f6db8f40cb8c992963610c5725e13ccfcddccb24b1cd", size = 61640, upload-time = "2025-11-08T13:54:03.202Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +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 = "kicoil" +version = "0.9.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "gerbonara" }, +] + +[package.metadata] +requires-dist = [ + { name = "click" }, + { name = "gerbonara", specifier = ">=1.6.0" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "priority" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/3c/eb7c35f4dcede96fca1842dac5f4f5d15511aa4b52f3a961219e68ae9204/priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0", size = 24792, upload-time = "2021-06-27T10:15:05.487Z" } +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 = "quart" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "blinker" }, + { name = "click" }, + { name = "flask" }, + { name = "hypercorn" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/9d/12e1143a5bd2ccc05c293a6f5ae1df8fd94a8fc1440ecc6c344b2b30ce13/quart-0.20.0.tar.gz", hash = "sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d", size = 63874, upload-time = "2024-12-23T13:53:05.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e9/cc28f21f52913adf333f653b9e0a3bf9cb223f5083a26422968ba73edd8d/quart-0.20.0-py3-none-any.whl", hash = "sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1", size = 77960, upload-time = "2024-12-23T13:53:02.842Z" }, +] + +[[package]] +name = "rtree" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/09/7302695875a019514de9a5dd17b8320e7a19d6e7bc8f85dcfb79a4ce2da3/rtree-1.4.1.tar.gz", hash = "sha256:c6b1b3550881e57ebe530cc6cffefc87cd9bf49c30b37b894065a9f810875e46", size = 52425, upload-time = "2025-08-13T19:32:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d9/108cd989a4c0954e60b3cdc86fd2826407702b5375f6dfdab2802e5fed98/rtree-1.4.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:d672184298527522d4914d8ae53bf76982b86ca420b0acde9298a7a87d81d4a4", size = 468484, upload-time = "2025-08-13T19:31:50.593Z" }, + { url = "https://files.pythonhosted.org/packages/f3/cf/2710b6fd6b07ea0aef317b29f335790ba6adf06a28ac236078ed9bd8a91d/rtree-1.4.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7e48d805e12011c2cf739a29d6a60ae852fb1de9fc84220bbcef67e6e595d7d", size = 436325, upload-time = "2025-08-13T19:31:52.367Z" }, + { url = "https://files.pythonhosted.org/packages/55/e1/4d075268a46e68db3cac51846eb6a3ab96ed481c585c5a1ad411b3c23aad/rtree-1.4.1-py3-none-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa8c4496e31e9ad58ff6c7df89abceac7022d906cb64a3e18e4fceae6b77f65", size = 459789, upload-time = "2025-08-13T19:31:53.926Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/e5d44be90525cd28503e7f836d077ae6663ec0687a13ba7810b4114b3668/rtree-1.4.1-py3-none-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12de4578f1b3381a93a655846900be4e3d5f4cd5e306b8b00aa77c1121dc7e8c", size = 507644, upload-time = "2025-08-13T19:31:55.164Z" }, + { url = "https://files.pythonhosted.org/packages/fd/85/b8684f769a142163b52859a38a486493b05bafb4f2fb71d4f945de28ebf9/rtree-1.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b558edda52eca3e6d1ee629042192c65e6b7f2c150d6d6cd207ce82f85be3967", size = 1454478, upload-time = "2025-08-13T19:31:56.808Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/c2292b95246b9165cc43a0c3757e80995d58bc9b43da5cb47ad6e3535213/rtree-1.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f155bc8d6bac9dcd383481dee8c130947a4866db1d16cb6dff442329a038a0dc", size = 1555140, upload-time = "2025-08-13T19:31:58.031Z" }, + { url = "https://files.pythonhosted.org/packages/74/25/5282c8270bfcd620d3e73beb35b40ac4ab00f0a898d98ebeb41ef0989ec8/rtree-1.4.1-py3-none-win_amd64.whl", hash = "sha256:efe125f416fd27150197ab8521158662943a40f87acab8028a1aac4ad667a489", size = 389358, upload-time = "2025-08-13T19:31:59.247Z" }, + { 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 = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] + +[[package]] +name = "wsproto" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, +]