diff --git a/.github/workflows/python-pytest.yml b/.github/workflows/python-pytest.yml index ead0d21..5d9e1dd 100644 --- a/.github/workflows/python-pytest.yml +++ b/.github/workflows/python-pytest.yml @@ -15,23 +15,19 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest, ubuntu-latest] - python-version: ["3.10", "3.11", "3.12"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + - name: Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install uv - python -m uv pip install . - python -m uv pip install black + uv sync - name: Test black formatted run: | - black src --check + uv run black src --check - name: Test with pytest run: | - pytest + uv run pytest diff --git a/pyproject.toml b/pyproject.toml index c8adb9a..9011310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,13 +11,12 @@ dependencies = [ "shapely>=2.0.0" ] -[project.optional-dependencies] - - -test = [ - "pytest >= 8.0.0", -] - [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" + +[dependency-groups] +dev = [ + "black>=25.1.0", + "pytest>=8.4.1", +] diff --git a/src/load_distribution/__init__.py b/src/load_distribution/__init__.py index bff492b..0401d92 100644 --- a/src/load_distribution/__init__.py +++ b/src/load_distribution/__init__.py @@ -5,4 +5,4 @@ __version__ = "0.1.0" -from load_distribution.load_distribution import * \ No newline at end of file +from load_distribution.load_distribution import * diff --git a/src/load_distribution/geom_ops.py b/src/load_distribution/geom_ops.py index e2ccdc9..078cd0d 100644 --- a/src/load_distribution/geom_ops.py +++ b/src/load_distribution/geom_ops.py @@ -13,7 +13,7 @@ box, convex_hull, intersects, - union + union, ) import shapely.ops as ops import shapely.affinity as aff @@ -21,11 +21,16 @@ Geometry = Union[LineString, Polygon] IntersectingGeometry = Union[Point, LineString] + class GeometryError(Exception): pass + def get_intersection( - above: Geometry, below: Geometry, j_tag: str, above_extent_polygon: Optional[Polygon] = None + above: Geometry, + below: Geometry, + j_tag: str, + above_extent_polygon: Optional[Polygon] = None, ) -> Optional[tuple[str, IntersectingGeometry, Geometry]]: """ Returns the details of the intersection @@ -34,7 +39,7 @@ def get_intersection( i_type = above.geom_type j_type = below.geom_type i_extent = above_extent_polygon - if i_extent and j_type=="Polygon": + if i_extent and j_type == "Polygon": # Goal: calculate the intersecting region as being along the centerline # of the linear polygon support so that, down the line, it becomes easy # to calculate the extents from the intersecting region @@ -42,8 +47,12 @@ def get_intersection( inter_centerline = get_rectangle_centerline(intersecting_region) support_centerline = get_rectangle_centerline(below) # Project the intersecting region centerline onto the support centerline - _, projected_a = ops.nearest_points(Point(inter_centerline.coords[0]), support_centerline) - _, projected_b = ops.nearest_points(Point(inter_centerline.coords[1]), support_centerline) + _, projected_a = ops.nearest_points( + Point(inter_centerline.coords[0]), support_centerline + ) + _, projected_b = ops.nearest_points( + Point(inter_centerline.coords[1]), support_centerline + ) intersecting_region = LineString([projected_a, projected_b]) elif i_extent and j_type == "LineString": intersecting_region = i_extent.intersection(below) @@ -60,11 +69,11 @@ def get_intersection( all_linestrings = i_type == j_type == "LineString" if intersecting_region.geom_type == "Point" and all_linestrings: return (intersecting_region, below, j_tag) - elif intersecting_region.geom_type == "MultiPoint": # Line enters and exits a polygon boundary - if ( - (i_type == "Polygon" and j_type == "LineString") - or - (i_type == "LineString" and j_type == "Polygon") + elif ( + intersecting_region.geom_type == "MultiPoint" + ): # Line enters and exits a polygon boundary + if (i_type == "Polygon" and j_type == "LineString") or ( + i_type == "LineString" and j_type == "Polygon" ): point = intersecting_region.centroid return (point, below, j_tag) @@ -75,15 +84,21 @@ def get_intersection( ) elif intersecting_region.geom_type == "LineString": return (intersecting_region, below, j_tag) - elif intersecting_region.geom_type == "Point": # LineString and Polygon intersection @ boundary + elif ( + intersecting_region.geom_type == "Point" + ): # LineString and Polygon intersection @ boundary return (intersecting_region, below, j_tag) - elif intersecting_region.geom_type == "Polygon": # Polygon point/line load intersecting with another polygon + elif ( + intersecting_region.geom_type == "Polygon" + ): # Polygon point/line load intersecting with another polygon return (intersecting_region, below, j_tag) else: return None -def check_corresponds(above: Union[LineString, Polygon], below: Union[LineString, Polygon]) -> float: +def check_corresponds( + above: Union[LineString, Polygon], below: Union[LineString, Polygon] +) -> float: """ Returns the ratio of overlap between geometry above and the geometry below. @@ -100,7 +115,7 @@ def check_corresponds(above: Union[LineString, Polygon], below: Union[LineString of the alignment of the sketch from plane to plane and does not necessarily represent the bearing area at a connection. For example, a ratio of 1.0 between two polygons representing columns may indicate that there is no "slope" in the column and that the bottom of the column has - been sketched so that it is directly under the top of the column. + been sketched so that it is directly under the top of the column. """ intersecting_region = above.intersection(below) a_type = above.geom_type @@ -108,21 +123,21 @@ def check_corresponds(above: Union[LineString, Polygon], below: Union[LineString c_type = intersecting_region.geom_type if intersecting_region is None: return 0.0 - elif a_type == b_type == c_type == 'LineString': + elif a_type == b_type == c_type == "LineString": return intersecting_region.length / below.length - elif a_type == b_type == c_type == "Polygon" : + elif a_type == b_type == c_type == "Polygon": return intersecting_region.area / below.area else: return 0.0 - -def get_local_intersection_ordinates(start_node: Point, intersections: list[Point]) -> list[float]: + +def get_local_intersection_ordinates( + start_node: Point, intersections: list[Point] +) -> list[float]: """ Returns the relative distances of the Points in 'intersections' relative to the 'start_node'. """ - return [ - start_node.distance(intersection) for intersection in intersections - ] + return [start_node.distance(intersection) for intersection in intersections] def get_linestring_start_node(ls: LineString) -> Point: @@ -136,13 +151,14 @@ def get_linestring_start_node(ls: LineString) -> Point: return start_coord - -def clean_polygon_supports(support_geoms: list[LineString | Polygon], joist_prototype: LineString): +def clean_polygon_supports( + support_geoms: list[LineString | Polygon], joist_prototype: LineString +): """ Converts any Polygon in support_geoms into LineStrings. The LineStrings are created depending on where the joist prototype lands within the polygon. - Assumption: the Polygon represents a single rectangle which represents a + Assumption: the Polygon represents a single rectangle which represents a wall or something similar. The resulting LineString will either be located on the inside face of the @@ -156,7 +172,7 @@ def clean_polygon_supports(support_geoms: list[LineString | Polygon], joist_prot if support_geom.geom_type == "Polygon": support_lines = explode_polygon(support_geom) support_intersections = joist_prototype.intersects(np.array(support_lines)) - if sum(support_intersections) == 1: # Intersects on one edge only + if sum(support_intersections) == 1: # Intersects on one edge only intersecting_line_index = int(support_intersections.nonzero()[0][0]) support_line = support_lines[intersecting_line_index] # Ensure there are no missing intersections on the support line @@ -164,7 +180,9 @@ def clean_polygon_supports(support_geoms: list[LineString | Polygon], joist_prot # elif sum(support_intersections) == 2: elif sum(support_intersections) == 0: assert support_geom.intersects(support_lines) - raise GeometryError(f"The geometry {support_geom.wkt} does not intersect {joist_prototype.wkt}") + raise GeometryError( + f"The geometry {support_geom.wkt} does not intersect {joist_prototype.wkt}" + ) elif sum(support_intersections) == 2: # Ensure there are no missing intersections on the support line # Can sometimes be caused by a joist intersecting with a column @@ -199,8 +217,12 @@ def get_joist_extents( right_coords = [] for joist_support in joist_supports: start_coord, end_coord = joist_support.coords - start_coord_rotation = cross_product_2d(joist_vector, np.array(start_coord) - joist_origin) - end_coord_rotation = cross_product_2d(joist_vector, np.array(end_coord) - joist_origin) + start_coord_rotation = cross_product_2d( + joist_vector, np.array(start_coord) - joist_origin + ) + end_coord_rotation = cross_product_2d( + joist_vector, np.array(end_coord) - joist_origin + ) if 0.0 < start_coord_rotation: left_coords.append(start_coord) elif start_coord_rotation < 0.0: @@ -209,19 +231,43 @@ def get_joist_extents( left_coords.append(end_coord) elif end_coord_rotation < 0.0: right_coords.append(end_coord) - closest_left_coord = min(left_coords, key=lambda x: Point(x).distance(joist_prototype)) - closest_right_coord = min(right_coords, key=lambda x: Point(x).distance(joist_prototype)) + closest_left_coord = min( + left_coords, key=lambda x: Point(x).distance(joist_prototype) + ) + closest_right_coord = min( + right_coords, key=lambda x: Point(x).distance(joist_prototype) + ) closest_left_distance = Point(closest_left_coord).distance(joist_prototype) closest_right_distance = Point(closest_right_coord).distance(joist_prototype) joist_vector_normal = rotate_90_vector(joist_vector, ccw=True) - joist_left = LineString([ - project_node(Point(joist_origin), joist_vector_normal, magnitude=closest_left_distance - eps), - project_node(Point(joist_end), joist_vector_normal, magnitude=closest_left_distance - eps) - ]) - joist_right = LineString([ - project_node(Point(joist_origin), -joist_vector_normal, magnitude=closest_right_distance - eps), - project_node(Point(joist_end), -joist_vector_normal, magnitude=closest_right_distance - eps) - ]) + joist_left = LineString( + [ + project_node( + Point(joist_origin), + joist_vector_normal, + magnitude=closest_left_distance - eps, + ), + project_node( + Point(joist_end), + joist_vector_normal, + magnitude=closest_left_distance - eps, + ), + ] + ) + joist_right = LineString( + [ + project_node( + Point(joist_origin), + -joist_vector_normal, + magnitude=closest_right_distance - eps, + ), + project_node( + Point(joist_end), + -joist_vector_normal, + magnitude=closest_right_distance - eps, + ), + ] + ) ordered_joist_supports = sort_supports(joist_prototype, joist_supports) extents = [] # from IPython.display import display @@ -253,7 +299,7 @@ def get_cantilever_segments( cantilever_segments = {"A": 0.0, "B": 0.0} if isinstance(cantilevers, LineString): split_a = cantilevers - split_b = Point() # A geometry of length 0 + split_b = Point() # A geometry of length 0 elif hasattr(cantilevers, "geoms"): split_a, split_b = cantilevers.geoms if split_a.distance(ordered_supports[0]) < split_a.distance(ordered_supports[-1]): @@ -264,29 +310,32 @@ def get_cantilever_segments( def find_extent_intersections( - element_geoms: list[LineString], - extent_geoms: list[LineString] - ) -> list[Optional[LineString]]: + element_geoms: list[LineString], extent_geoms: list[LineString] +) -> list[Optional[LineString]]: """ Returns a list of LineString representing the extent geometries put into the order of the element geometries. If an extent geometry intersects with an element_geometry, the resulting list will have the extent geometry in the same corresponding list position that the element geometry is in. If there is no such intersection, then that list position will be None. - """ + """ extents_array = np.array(extent_geoms) acc = [] for element_geom in element_geoms: mask = intersects(element_geom, extents_array) if mask.any(): - extent = extents_array[mask][0] # Assume the first one until a better idea comes + extent = extents_array[mask][ + 0 + ] # Assume the first one until a better idea comes acc.append(extent) else: acc.append(None) return acc -def create_extent_polygon(element_geom: LineString, extent_geom: Optional[LineString] = None) -> Polygon: +def create_extent_polygon( + element_geom: LineString, extent_geom: Optional[LineString] = None +) -> Polygon: """ Returns a Polygon representing the bounding box of the union of 'element_geom' and 'extent_geom' @@ -294,7 +343,7 @@ def create_extent_polygon(element_geom: LineString, extent_geom: Optional[LineSt if extent_geom is None: return None return box(*(union(element_geom, extent_geom).bounds)) - + def get_system_bounds( joist_prototype: LineString, joist_supports: list[LineString] @@ -361,7 +410,7 @@ def get_direction_vector(ls: LineString) -> np.ndarray: i_node, j_node = get_start_end_nodes(ls) column_vector = np.array(j_node.coords[0]) - np.array(i_node.coords[0]) column_vector_norm = np.linalg.norm(column_vector) - parallel_vector = column_vector / column_vector_norm + parallel_vector = column_vector / column_vector_norm return parallel_vector # return column_vector.T[0] # Return a flat, 1D vector @@ -375,9 +424,7 @@ def sort_supports( docstring for get_start_end_nodes for more explanation of the +ve vector direction. """ all_supports = MultiLineString(supports) - ordered_intersections = order_nodes_positive( - (joist_prototype & all_supports).geoms - ) + ordered_intersections = order_nodes_positive((joist_prototype & all_supports).geoms) ordered_supports = [] for point in ordered_intersections: for linestring in supports: @@ -429,7 +476,7 @@ def scale_vertices( vertices: list[Decimal], scale: Decimal, paper_origin: Optional[tuple[Decimal, Decimal]] = None, - round_precision: int = 4 + round_precision: int = 4, ) -> tuple[Decimal | float]: """ Scale the vertices in relation to the origin or in relation to 'paper_origin'. @@ -460,8 +507,9 @@ def _translate_vertices( return flattened_array - -def _group_vertices(vertices: list[Decimal | float], close=False) -> list[tuple[Decimal, Decimal]]: +def _group_vertices( + vertices: list[Decimal | float], close=False +) -> list[tuple[Decimal, Decimal]]: """ Returns a list of (x, y) tuples from a list of vertices in the format of: 'x1 y1 x2 y2 x3 y3 ... xn yn' @@ -490,7 +538,7 @@ def vertices_to_array(vertices: list[Decimal]) -> ArrayLike: def flatten_vertex_array(v: ArrayLike, precision=6) -> tuple[Decimal]: """ - Returns a flattened version of 'v' in the format of + Returns a flattened version of 'v' in the format of (x1, y1, x2, y2, x3, y3, ..., xn, yn) where 'v' is either a row or column-based vector of shape (2, n) or (n, 2) rounded to 'precision'. @@ -501,7 +549,6 @@ def flatten_vertex_array(v: ArrayLike, precision=6) -> tuple[Decimal]: return tuple([round(Decimal(x), precision) for x in v.flatten()]) - def _group_vertices_str(vertices: str, close=False) -> str: """ Returns a list of (x, y) tuples from a list of vertices in the format of: @@ -520,6 +567,7 @@ def _group_vertices_str(vertices: str, close=False) -> str: acc.append(acc[0]) return ", ".join(acc) + def rotate_90_vector(v: ArrayLike, precision: int = 6, ccw=True) -> tuple[float, float]: """ Rotate the vector components, 'x1' and 'y1' by 90 degrees. @@ -529,9 +577,9 @@ def rotate_90_vector(v: ArrayLike, precision: int = 6, ccw=True) -> tuple[float, """ # v_angle = np.arctan2(v[1], v[0]) if ccw: - angle = math.pi/2 + angle = math.pi / 2 else: - angle = -math.pi / 2 + angle = -math.pi / 2 rot = np.array( [ [round(math.cos(angle), precision), -round(math.sin(angle), precision)], @@ -550,9 +598,9 @@ def rotate_90_coords(v: ArrayLike, precision: int = 6, ccw=True) -> tuple[float, """ # v_angle = np.arctan2(v[1], v[0]) if ccw: - angle = math.pi/2 + angle = math.pi / 2 else: - angle = -math.pi / 2 + angle = -math.pi / 2 rot = np.array( [ [round(math.cos(angle), precision), -round(math.sin(angle), precision)], @@ -575,11 +623,19 @@ def rotate_to_horizontal(line: LineString, geoms: list[Geometry]): angle = math.atan2(delta_y, delta_x) - rotated_line = aff.translate(aff.rotate(line, -angle, origin=i_end, use_radians=True), xoff=-ix) - rotated_geoms = [aff.translate(aff.rotate(geom, -angle, origin=i_end, use_radians=True), xoff=-ix) for geom in geoms] + rotated_line = aff.translate( + aff.rotate(line, -angle, origin=i_end, use_radians=True), xoff=-ix + ) + rotated_geoms = [ + aff.translate( + aff.rotate(geom, -angle, origin=i_end, use_radians=True), xoff=-ix + ) + for geom in geoms + ] return rotated_line, rotated_geoms + def explode_polygon(p: Polygon) -> list[LineString]: """ Explodes the exterior of the polygon in to a list of individual line segments @@ -588,6 +644,7 @@ def explode_polygon(p: Polygon) -> list[LineString]: exploded = [LineString(tup) for tup in zip(ext_ls.coords, ext_ls.coords[1:])] return exploded + def get_rectangle_centerline(p: Polygon) -> LineString: """ Returns the centerline of the Polygon 'p' assuming that 'p' represents @@ -601,11 +658,9 @@ def get_rectangle_centerline(p: Polygon) -> LineString: start, end = order_nodes_positive([edge1.centroid, edge2.centroid]) center_line = LineString([start, end]) return center_line - -def calculate_trapezoid_area_sums( - member_loads: list[list[list[tuple]]] -) -> list[float]: + +def calculate_trapezoid_area_sums(member_loads: list[list[list[tuple]]]) -> list[float]: """ Returns a list of the sums of the areas of the trapezoids in 'traps' @@ -624,7 +679,6 @@ def calculate_trapezoid_area_sums( polygon_loads.append(trap_area) member_polys.append(sum(polygon_loads)) return member_polys - def trapezoid_area(h: float, b2: float, b1: float) -> float: @@ -644,9 +698,11 @@ def get_vector_angle(v1, v2) -> float: angle = np.arccos(num / denom) return angle + def cross_product_2d(v1, v2): return v1[0] * v2[1] - v2[0] * v1[1] + def create_linestring(points: list[tuple]) -> LineString: return LineString(points) diff --git a/src/load_distribution/load_distribution.py b/src/load_distribution/load_distribution.py index 98ad162..f1ee0bd 100644 --- a/src/load_distribution/load_distribution.py +++ b/src/load_distribution/load_distribution.py @@ -3,11 +3,7 @@ from dataclasses import dataclass import numpy.typing as npt from load_distribution import geom_ops -from shapely.geometry import ( - Polygon, - MultiPolygon, - LineString -) +from shapely.geometry import Polygon, MultiPolygon, LineString import itertools @@ -16,6 +12,7 @@ class LoadingGeometry: """ Represents a LoadingGeometry """ + geometry: Polygon | LineString occupancy: str load_components: npt.ArrayLike @@ -32,6 +29,7 @@ class DistributedLoad: 'w0': The starting magnitude 'w1': The end magnitude """ + x0: float x1: float w0: float @@ -61,6 +59,7 @@ class Overlap: OL0 = Overlap(x0=-5.0, x1=10.0, ma=-4.0, ba=15.0, mb=2, bb=-2.0) OL1 = Overlap(x0=12.3, x1=16.3, ma=0.5, ba=6.1, mb=-3.34, bb=2.5) + @dataclass class Singularity: """ @@ -115,7 +114,7 @@ def get_distributed_loads_from_projected_polygons( # Apply a unit load for all distributed loads poly_xy = project_polygon(load_geom, 1.0, xy=True) projected_poly_coords = list(zip(*poly_xy)) - polygon_dist_loads = [] # Polygon may have many pairs + polygon_dist_loads = [] # Polygon may have many pairs inner_pair = [] for idx, coord in enumerate(projected_poly_coords[1:-1]): if idx % 2 == 1: @@ -392,4 +391,4 @@ def get_void_regions(p: Polygon) -> list[Polygon]: elif isinstance(void_regions, MultiPolygon): return list(void_regions.geoms) else: - return [] \ No newline at end of file + return [] diff --git a/tests/test_geom.py b/tests/test_geom.py index a14ab5f..08d1140 100644 --- a/tests/test_geom.py +++ b/tests/test_geom.py @@ -5,6 +5,7 @@ from load_distribution import geom_ops + def test_check_corresponds(): ls1 = LineString([[0, 0], [1, 0]]) @@ -24,13 +25,13 @@ def test_check_corresponds(): def test_get_joist_extents(): - ls1 = LineString([[50, 4], [300,56]]) + ls1 = LineString([[50, 4], [300, 56]]) ls2 = LineString([[-23, 300], [350, 335]]) j1 = LineString([[140.0, -23.4], [100.0, 390.3]]) extents = geom_ops.get_joist_extents(j1, [ls1, ls2]) assert ( - wkt.dumps(MultiPoint(extents[0] + extents[1]), trim=True, rounding_precision=3) - == 'MULTIPOINT ((273.716 327.842), (20.981 304.127), (300 56), (50 4))' + wkt.dumps(MultiPoint(extents[0] + extents[1]), trim=True, rounding_precision=3) + == "MULTIPOINT ((273.716 327.842), (20.981 304.127), (300 56), (50 4))" ) ls1 = LineString([[0, 0], [0, 100]]) ls2 = LineString([[50, -20], [50, 80]]) @@ -38,7 +39,7 @@ def test_get_joist_extents(): extents = geom_ops.get_joist_extents(j1, [ls1, ls2]) assert ( wkt.dumps(MultiPoint(extents[0] + extents[1]), trim=True, rounding_precision=3) - == 'MULTIPOINT ((0 80), (0 10e-7), (50 80), (50 10e-7))' + == "MULTIPOINT ((0 80), (0 10e-7), (50 80), (50 10e-7))" ) @@ -53,4 +54,4 @@ def test_order_nodes_positive(): assert geom_ops.order_nodes_positive([p6, p1]) == (p1, p6) assert geom_ops.order_nodes_positive([p4, p3]) == (p3, p4) assert geom_ops.order_nodes_positive([p6, p5]) == (p6, p5) - assert geom_ops.order_nodes_positive([p2, p1]) == (p1, p2) \ No newline at end of file + assert geom_ops.order_nodes_positive([p2, p1]) == (p1, p2) diff --git a/uv.lock b/uv.lock index f64fb17..096e313 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,46 @@ version = 1 requires-python = ">=3.11" +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -28,18 +68,33 @@ dependencies = [ { name = "shapely" }, ] -[package.optional-dependencies] -test = [ +[package.dev-dependencies] +dev = [ + { name = "black" }, { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "numpy", specifier = ">=2.0.0" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=8.0.0" }, { name = "shapely", specifier = ">=2.0.0" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=25.1.0" }, + { name = "pytest", specifier = ">=8.4.1" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + [[package]] name = "numpy" version = "2.3.2" @@ -130,6 +185,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654 }, +] + [[package]] name = "pluggy" version = "1.6.0"