Compare commits

..

1 Commits

Author SHA1 Message Date
Dogukan Karatas 1ce1c36a51 updated polyline class and tests 2025-03-12 18:22:31 +01:00
18 changed files with 68 additions and 430 deletions
-2
View File
@@ -11,7 +11,5 @@ jobs:
# Orchestrate our job run sequence
workflows:
build_and_test:
when:
false
jobs:
- build
-2
View File
@@ -2,8 +2,6 @@
.envrc
reports/
.volumes/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+3 -4
View File
@@ -13,7 +13,7 @@ services:
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- ./.volumes/postgres-data:/var/lib/postgresql/data/
- postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
@@ -25,7 +25,7 @@ services:
image: "redis:6.0-alpine"
restart: always
volumes:
- ./.volumes/redis-data:/data
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
@@ -37,7 +37,7 @@ services:
command: server /data --console-address ":9001"
restart: always
volumes:
- ./.volumes/minio-data:/data
- minio-data:/data
healthcheck:
test:
[
@@ -100,7 +100,6 @@ services:
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
FRONTEND_ORIGIN: "http://127.0.0.1:8080"
networks:
default:
+1
View File
@@ -15,6 +15,7 @@ dependencies = [
"httpx>=0.28.1",
"pydantic>=2.10.5",
"pydantic-settings>=2.7.1",
"stringcase>=1.2.0",
"ujson>=5.10.0",
]
+4 -4
View File
@@ -4,13 +4,13 @@ from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
@@ -39,7 +39,7 @@ class AutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
@@ -52,7 +52,7 @@ class TestAutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
class ProjectVisibility(str, Enum):
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
UNLISTED = "UNLISTED"
UNLISTEd = "UNLISTED"
class UserProjectsUpdatedMessageType(str, Enum):
-10
View File
@@ -82,16 +82,6 @@ class LimitedUser(GraphQLBaseModel):
verified: Optional[bool]
role: Optional[str]
def __repr__(self):
return (
f"(name: {self.name}, "
f"id: {self.id}, "
f"bio: {self.bio}, "
f"company: {self.company}, "
f"verified: {self.verified}, "
f"role: {self.role})"
)
class PendingStreamCollaborator(GraphQLBaseModel):
id: str
+2 -2
View File
@@ -17,7 +17,7 @@ from typing import (
)
from warnings import warn
from pydantic.alias_generators import to_pascal
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.memory import MemoryTransport
@@ -147,7 +147,7 @@ class _RegisteringBase:
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
to_pascal(m)
pascalcase(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
@@ -2,7 +2,6 @@ from .arc import Arc
from .box import Box
from .circle import Circle
from .control_point import ControlPoint
from .curve import Curve
from .ellipse import Ellipse
from .line import Line
from .mesh import Mesh
@@ -11,7 +10,6 @@ from .point import Point
from .point_cloud import PointCloud
from .polycurve import Polycurve
from .polyline import Polyline
from .region import Region
from .spiral import Spiral
from .surface import Surface
from .vector import Vector
@@ -24,7 +22,6 @@ __all__ = [
"Plane",
"Point",
"Polyline",
"Region",
"Vector",
"Box",
"Circle",
@@ -34,5 +31,4 @@ __all__ = [
"Polycurve",
"Spiral",
"Surface",
"Curve",
]
-58
View File
@@ -1,58 +0,0 @@
from dataclasses import dataclass
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry.box import Box
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Curve(
Base,
ICurve,
IHasArea,
IHasUnits,
speckle_type="Objects.Geometry.Curve",
detachable={"points", "weights", "knots", "displayValue"},
chunkable={"points": 31250, "weights": 31250, "knots": 31250},
):
"""
a NURBS curve
"""
degree: int
periodic: bool
rational: bool
points: List[float]
weights: List[float]
knots: List[float]
closed: bool
displayValue: Polyline
bbox: Optional[Box] = None
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"degree: {self.degree}, "
f"periodic: {self.periodic}, "
f"rational: {self.rational}, "
f"closed: {self.closed}, "
f"units: {self.units})"
)
@property
def length(self) -> float:
return self.__dict__.get("_length", 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__["_length"] = value
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
+16 -11
View File
@@ -4,6 +4,7 @@ from typing import List
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
from specklepy.objects.models.units import Units
@dataclass(kw_only=True)
@@ -13,24 +14,28 @@ class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"
"""
value: List[float]
closed: bool = False
def __repr__(self) -> str:
return f"{self.__class__.__name__}(value: {self.value}, units: {self.units})"
return (
f"{self.__class__.__name__}("
f"value: {self.value}, "
f"closed: {self.closed}, "
f"units: {self.units})"
)
def is_closed(self, tolerance: float = 1e-6) -> bool:
@staticmethod
def is_closed(points: List[float], tolerance: float = 1e-6) -> bool:
"""
check if the polyline is closed (start point equals end point within tolerance)
check if the polyline is closed
"""
if len(self.value) < 6: # need at least 2 points to be closed
if len(points) < 6: # need at least 2 points to be closed
return False
# compare first and last points
start = Point(
x=self.value[0], y=self.value[1], z=self.value[2], units=self.units
)
end = Point(
x=self.value[-3], y=self.value[-2], z=self.value[-1], units=self.units
)
start = Point(x=points[0], y=points[1], z=points[2], units=Units.m)
end = Point(x=points[-3], y=points[-2], z=points[-1], units=Units.m)
return start.distance_to(end) <= tolerance
@property
@@ -46,7 +51,7 @@ class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"
total_length = 0.0
for i in range(len(points) - 1):
total_length += points[i].distance_to(points[i + 1])
if self.is_closed() and points:
if self.closed and points:
total_length += points[-1].distance_to(points[0])
return total_length
-60
View File
@@ -1,60 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.objects.geometry.box import Box
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.interfaces import ICurve, IDisplayValue, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Region(
Base,
IHasArea,
IDisplayValue[List[Mesh]],
IHasUnits,
speckle_type="Objects.Geometry.Region",
detachable={"displayValue"},
):
"""
Flat shape, defined by an outer boundary and inner loops.
"""
boundary: ICurve
innerLoops: List[ICurve]
hasHatchPattern: bool
bbox: Box | None = None
# unlike C#, constructor will require displayValue, even if it's empty
displayValue: List[Mesh]
_displayValue: List[Mesh] = field(repr=False, init=False)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"units: {self.units}, "
f"has_hatch_pattern: {self.hasHatchPattern}, "
f"inner_loops: {len(self.innerLoops)})"
)
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
@property
def displayValue(self) -> List[Mesh]:
print(self._displayValue)
return self._displayValue
@displayValue.setter
def displayValue(self, value: list):
if isinstance(value, list):
self._displayValue = value
else:
raise SpeckleException(
f"'displayValue' value should be List, received {type(value)}"
)
-158
View File
@@ -1,158 +0,0 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Curve, Plane, Point, Polyline, Vector
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_polyline():
"""
sample polyline
"""
return Polyline(value=[0, 0, 0, 1, 0, 0, 1, 1, 0], units=Units.m)
@pytest.fixture
def sample_plane():
"""
sample plane for bbox creation
"""
origin = Point(x=0, y=0, z=0, units=Units.m)
normal = Vector(x=0, y=0, z=1, units=Units.m)
xdir = Vector(x=1, y=0, z=0, units=Units.m)
ydir = Vector(x=0, y=1, z=0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_curve(sample_polyline):
"""
sample curve for testing
"""
return Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
def test_curve_creation(sample_polyline):
"""
test curve initialization
"""
curve = Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.degree == 3
assert curve.periodic is False
assert curve.rational is False
assert curve.points == [0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0]
assert curve.weights == [1, 1, 1, 1]
assert curve.knots == [0, 0, 0, 0, 1, 1, 1, 1]
assert curve.closed is False
assert curve.units == Units.m.value
assert curve.displayValue == sample_polyline
def test_length_property(sample_polyline):
"""
test the length property setter and getter
"""
curve = Curve(
degree=1,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 0, 0],
weights=[1, 1],
knots=[0, 0, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.length == 0.0
curve.length = 1.5
assert curve.length == 1.5
def test_area_property(sample_polyline):
"""
test the area property setter and getter
"""
polyline = Polyline(
value=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0], units=Units.m
)
curve = Curve(
degree=1,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0],
weights=[1, 1, 1, 1, 1],
knots=[0, 0, 1, 2, 3, 4, 4],
closed=True,
displayValue=polyline,
units=Units.m,
)
assert curve.area == 0.0
curve.area = 1.0
assert curve.area == 1.0
def test_curve_serialization(sample_curve):
"""
test serialization and deserialization of the curve
"""
serialized = serialize(sample_curve)
deserialized = deserialize(serialized)
assert deserialized.degree == sample_curve.degree
assert deserialized.periodic == sample_curve.periodic
assert deserialized.rational == sample_curve.rational
assert deserialized.points == sample_curve.points
assert deserialized.weights == sample_curve.weights
assert deserialized.knots == sample_curve.knots
assert deserialized.closed == sample_curve.closed
assert deserialized.units == sample_curve.units
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_curve_units(sample_polyline, new_units):
"""
test changing units of a curve
"""
curve = Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.units == Units.m.value
curve.units = new_units
assert curve.units == new_units
+30 -13
View File
@@ -54,6 +54,7 @@ def test_polyline_creation(open_square_coords):
polyline = Polyline(value=open_square_coords, units=Units.m)
assert polyline.value == open_square_coords
assert polyline.units == Units.m.value
assert polyline.closed is False
def test_polyline_domain(sample_polyline):
@@ -63,23 +64,31 @@ def test_polyline_domain(sample_polyline):
def test_polyline_is_closed(open_square_coords, closed_square_coords):
open_poly = Polyline(value=open_square_coords, units=Units.m)
closed_poly = Polyline(value=closed_square_coords, units=Units.m)
# Test the static method
assert not Polyline.is_closed(open_square_coords)
assert Polyline.is_closed(closed_square_coords)
assert not open_poly.is_closed()
assert closed_poly.is_closed()
# Test with closed flag
open_poly = Polyline(value=open_square_coords, units=Units.m)
closed_poly = Polyline(value=closed_square_coords, units=Units.m, closed=True)
assert not open_poly.closed
assert closed_poly.closed
def test_polyline_is_closed_with_tolerance(open_square_coords):
almost_closed = open_square_coords + [
0.0,
0.0,
0.001,
] # last point slightly above start
poly = Polyline(value=almost_closed, units=Units.m)
0.001, # last point slightly above start
]
# Test static method with tolerance
assert not Polyline.is_closed(almost_closed, tolerance=1e-6)
assert Polyline.is_closed(almost_closed, tolerance=0.01)
assert not poly.is_closed(tolerance=1e-6)
assert poly.is_closed(tolerance=0.01)
# Also test with instance
poly = Polyline(value=almost_closed, units=Units.m)
# poly.closed should reflect what was passed in construction, not computed
assert not poly.closed
def test_polyline_length_open(sample_polyline):
@@ -88,14 +97,13 @@ def test_polyline_length_open(sample_polyline):
def test_polyline_length_closed(closed_square_coords):
polyline = Polyline(value=closed_square_coords, units=Units.m)
polyline = Polyline(value=closed_square_coords, units=Units.m, closed=True)
polyline.length = polyline.calculate_length()
assert polyline.length == 4.0
def test_polyline_get_points(sample_polyline):
points = sample_polyline.get_points()
assert len(points) == 4
assert all(isinstance(p, Point) for p in points)
assert all(p.units == Units.m.value for p in points)
@@ -125,16 +133,25 @@ def test_polyline_invalid_coordinates():
def test_polyline_units(open_square_coords):
polyline = Polyline(value=open_square_coords, units=Units.m)
assert polyline.units == Units.m.value
polyline.units = "mm"
assert polyline.units == "mm"
def test_polyline_closed_flag(open_square_coords, closed_square_coords):
# Test default value
poly1 = Polyline(value=open_square_coords, units=Units.m)
assert poly1.closed is False
# Test explicit value
poly2 = Polyline(value=closed_square_coords, units=Units.m, closed=True)
assert poly2.closed is True
def test_polyline_serialization(sample_polyline):
serialized = serialize(sample_polyline)
deserialized = deserialize(serialized)
assert deserialized.value == sample_polyline.value
assert deserialized.units == sample_polyline.units
assert deserialized.domain.start == sample_polyline.domain.start
assert deserialized.domain.end == sample_polyline.domain.end
assert deserialized.closed == sample_polyline.closed
@@ -1,84 +0,0 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.region import Region
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_boundary():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[-10, -10, 0, 10, -10, 0, 10, 10, 0, -10, 10, 0, -10, -10, 0],
units=Units.m,
)
@pytest.fixture
def sample_loop1():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[-9, -9, 0, -5, -9, 0, -5, -5, 0, -9, -5, 0, -9, -9, 0],
units=Units.m,
)
@pytest.fixture
def sample_loop2():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[5, 5, 0, 9, 5, 0, 9, 9, 0, 5, 9, 0, 5, 5, 0],
units=Units.m,
)
@pytest.fixture
def sample_region(sample_boundary, sample_loop1, sample_loop2):
return Region(
boundary=sample_boundary,
innerLoops=[sample_loop1, sample_loop2],
hasHatchPattern=True,
units=Units.m,
displayValue=[],
)
def test_region_creation(sample_boundary, sample_loop1, sample_loop2):
has_hatch_pattern = True
region = Region(
boundary=sample_boundary,
innerLoops=[sample_loop1, sample_loop2],
hasHatchPattern=has_hatch_pattern,
units=Units.m,
displayValue=[],
)
assert region.boundary == sample_boundary
assert region.innerLoops[0] == sample_loop1
assert region.innerLoops[1] == sample_loop2
assert region.hasHatchPattern == has_hatch_pattern
assert len(region.displayValue) == 0
assert region.units == Units.m.value
def test_region_serialization(sample_region):
serialized = serialize(sample_region)
deserialized = deserialize(serialized)
assert deserialized.hasHatchPattern == sample_region.hasHatchPattern
assert deserialized.units == sample_region.units
assert deserialized.boundary.length == sample_region.boundary.length
assert deserialized.boundary.domain.length == sample_region.boundary.domain.length
assert deserialized.boundary.domain.start == sample_region.boundary.domain.start
assert deserialized.boundary.domain.end == sample_region.boundary.domain.end
for i, loop in enumerate(sample_region.innerLoops):
assert deserialized.innerLoops[i].length == loop.length
assert deserialized.innerLoops[i].domain.length == loop.domain.length
assert deserialized.innerLoops[i].domain.start == loop.domain.start
assert deserialized.innerLoops[i].domain.end == loop.domain.end
+1 -6
View File
@@ -94,12 +94,6 @@ class ServerTransport(AbstractTransport):
self.session = requests.Session()
self.session.headers.update(
{
"Accept": "text/plain",
}
)
if self.account is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
@@ -107,6 +101,7 @@ class ServerTransport(AbstractTransport):
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@@ -27,7 +27,6 @@ class TestProjectResource:
"name, description, visibility",
[
("Very private project", "My secret project", ProjectVisibility.PRIVATE),
("Very discoverable project", None, ProjectVisibility.UNLISTED),
("Very public project", None, ProjectVisibility.PUBLIC),
],
)
@@ -49,11 +48,7 @@ class TestProjectResource:
assert result.id is not None
assert result.name == name
assert result.description == (description or "")
# we've disabled creation of public projects for now, they fall back to unlisted
if visibility == ProjectVisibility.PUBLIC:
assert result.visibility == ProjectVisibility.UNLISTED
else:
assert result.visibility == visibility
assert result.visibility == visibility
def test_project_get(self, client: SpeckleClient, test_project: Project):
result = client.project.get(test_project.id)
@@ -83,11 +78,7 @@ class TestProjectResource:
assert updated_project.id == test_project.id
assert updated_project.name == new_name
assert updated_project.description == new_description
# we've disabled creation of public projects for now, they fall back to unlisted
if new_visibility == ProjectVisibility.PUBLIC:
assert updated_project.visibility == ProjectVisibility.UNLISTED
else:
assert updated_project.visibility == new_visibility
assert updated_project.visibility == new_visibility
def test_project_delete(self, client: SpeckleClient):
"""Test deleting a project."""
Generated
+8
View File
@@ -1392,6 +1392,7 @@ dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "stringcase" },
{ name = "ujson" },
]
@@ -1421,6 +1422,7 @@ requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pydantic", specifier = ">=2.10.5" },
{ name = "pydantic-settings", specifier = ">=2.7.1" },
{ name = "stringcase", specifier = ">=1.2.0" },
{ name = "ujson", specifier = ">=5.10.0" },
]
@@ -1441,6 +1443,12 @@ dev = [
{ name = "types-ujson", specifier = ">=5.10.0.20240515" },
]
[[package]]
name = "stringcase"
version = "1.2.0"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/1f/1241aa3d66e8dc1612427b17885f5fcd9c9ee3079fc0d28e9a3aeeb36fa3/stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008", size = 2958 }
[[package]]
name = "termcolor"
version = "2.5.0"