Compare commits

...

15 Commits

Author SHA1 Message Date
Dogukan Karatas f7f9f73e7b feat(specklepy): curve object class (#400)
* adds curve class
2025-04-11 14:09:39 +02:00
Gergő Jedlicska a7bada391b Merge pull request #398 from specklesystems/gergo/nostringcase
gergo/nostringcase
2025-04-01 11:53:03 +02:00
Gergő Jedlicska 81ff5d82cb Merge pull request #399 from specklesystems/Skip-Circle-Ci
Update config.yml
2025-04-01 11:52:28 +02:00
Jedd Morgan d25edbb3d7 Update config.yml 2025-04-01 10:28:34 +01:00
Gergő Jedlicska 7dedff68f4 Merge branch 'v3-dev' of github.com:specklesystems/specklepy into gergo/nostringcase 2025-03-27 15:50:28 +01:00
Gergő Jedlicska d6e31a9752 chore: fix compose file 2025-03-27 15:19:25 +01:00
Gergő Jedlicska 09c61424d7 tests: update some tests with new server standards 2025-03-27 13:56:19 +01:00
Gergő Jedlicska e9bdf0ceb8 chore: update poetry lock 2025-03-24 20:22:03 +01:00
Gergő Jedlicska 7e6174ebc1 chore: remove stringcase as a dependency 2025-03-24 19:47:07 +01:00
Gergő Jedlicska b8ae3ca8c8 Merge pull request #395 from specklesystems/dogukan/override-limited-user-repr
fix (specklepy): removes avatar in version string representation
2025-03-17 18:31:58 +01:00
Dogukan Karatas d690c45b35 overrides repr 2025-03-17 15:37:13 +01:00
KatKatKateryna 5d3a824986 add region class and tests (#393)
* add region class and tests

* syntax

* export class

* typos
2025-03-17 19:32:57 +08:00
Dogukan Karatas 6f56ecb0c0 fix syntax (#392) 2025-03-11 11:40:25 +01:00
Gergő Jedlicska 6c33c61a6d Merge pull request #382 from specklesystems/gergo/fixServerTransportHeader
fix: server transport always accept text/plain
2025-02-12 13:03:00 +01:00
Gergő Jedlicska 71afb1275f fix: server transport always accept text/plain 2025-02-12 12:35:11 +01:00
17 changed files with 408 additions and 25 deletions
+2
View File
@@ -11,5 +11,7 @@ jobs:
# Orchestrate our job run sequence
workflows:
build_and_test:
when:
false
jobs:
- build
+2
View File
@@ -2,6 +2,8 @@
.envrc
reports/
.volumes/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+4 -3
View File
@@ -13,7 +13,7 @@ services:
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-data:/var/lib/postgresql/data/
- ./.volumes/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:
- redis-data:/data
- ./.volumes/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:
- minio-data:/data
- ./.volumes/minio-data:/data
healthcheck:
test:
[
@@ -100,6 +100,7 @@ services:
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
FRONTEND_ORIGIN: "http://127.0.0.1:8080"
networks:
default:
-1
View File
@@ -15,7 +15,6 @@ 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 stringcase import camelcase
from pydantic.alias_generators import to_camel
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
@@ -39,7 +39,7 @@ class AutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
)
@@ -52,7 +52,7 @@ class TestAutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
alias_generator=to_camel, 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,6 +82,16 @@ 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 stringcase import pascalcase
from pydantic.alias_generators import to_pascal
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(
pascalcase(m)
to_pascal(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
@@ -2,6 +2,7 @@ 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
@@ -10,6 +11,7 @@ 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
@@ -22,6 +24,7 @@ __all__ = [
"Plane",
"Point",
"Polyline",
"Region",
"Vector",
"Box",
"Circle",
@@ -31,4 +34,5 @@ __all__ = [
"Polycurve",
"Spiral",
"Surface",
"Curve",
]
+58
View File
@@ -0,0 +1,58 @@
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
+2 -3
View File
@@ -49,9 +49,8 @@ class Mesh(
if len(self.vertices) % 3 != 0:
raise ValueError(
f"Invalid vertices list: length ({
len(self.vertices)
}) must be a multiple of 3"
f"Invalid vertices list: length {len(self.vertices)} "
f"must be a multiple of 3"
)
return len(self.vertices) // 3
+60
View File
@@ -0,0 +1,60 @@
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
@@ -0,0 +1,158 @@
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
@@ -0,0 +1,84 @@
# 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
+6 -1
View File
@@ -94,6 +94,12 @@ 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
@@ -101,7 +107,6 @@ class ServerTransport(AbstractTransport):
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@@ -27,6 +27,7 @@ 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),
],
)
@@ -48,7 +49,11 @@ class TestProjectResource:
assert result.id is not None
assert result.name == name
assert result.description == (description or "")
assert result.visibility == visibility
# 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
def test_project_get(self, client: SpeckleClient, test_project: Project):
result = client.project.get(test_project.id)
@@ -78,7 +83,11 @@ class TestProjectResource:
assert updated_project.id == test_project.id
assert updated_project.name == new_name
assert updated_project.description == new_description
assert updated_project.visibility == new_visibility
# 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
def test_project_delete(self, client: SpeckleClient):
"""Test deleting a project."""
Generated
-8
View File
@@ -1392,7 +1392,6 @@ dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "stringcase" },
{ name = "ujson" },
]
@@ -1422,7 +1421,6 @@ 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" },
]
@@ -1443,12 +1441,6 @@ 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"