Make basic functionality work

This commit is contained in:
jaseg 2025-12-08 14:48:47 +01:00
parent f9a1d61e59
commit 1981512483
7 changed files with 687 additions and 369 deletions

View file

@ -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" },

6
src/kicoil/__init__.py Normal file
View file

@ -0,0 +1,6 @@
from importlib.metadata import version
from .geometry import PlanarInductor
__version__ = version('kicoil')

130
src/kicoil/cli.py Normal file
View file

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

View file

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

View file

@ -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])

View file

@ -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'<path {attrs} d="{self.d.rstrip()}"/>'
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'<circle {attrs} r="{self.r}" cx="{self.cx}" cy="{self.cy}"/>'
def svg_file(fn, stuff, vbw, vbh, vbx=0, vby=0):
with open(fn, 'w') as f:
f.write('<?xml version="1.0" standalone="no"?>\n')
f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
f.write(f'<svg version="1.1" width="{vbw*4}mm" height="{vbh*4}mm" viewBox="{vbx} {vby} {vbw} {vbh}" style="background-color: #333" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
for foo in stuff:
f.write(str(foo))
f.write('</svg>\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)

285
uv.lock generated Normal file
View file

@ -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" },
]