Compare commits

..

4 Commits

Author SHA1 Message Date
Jonathon Broughton e99efe8105 Add workspaceId to test automation run and schema 2026-04-07 22:03:13 +01:00
Jonathon Broughton 30de4d207a Handle generic exceptions in AutomationContext project retrieval 2026-04-07 22:03:12 +01:00
Jonathon Broughton 69829266a4 extend AutomationRunData model to include workspace_id 2026-04-07 21:59:18 +01:00
Jonathon Broughton ca1b8c52ed add workspace_id property and resolution logic in AutomationContext 2026-04-07 21:59:17 +01:00
9 changed files with 71 additions and 45 deletions
-4
View File
@@ -13,14 +13,12 @@ jobs:
name: Test (internal)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v4
@@ -64,14 +62,12 @@ jobs:
env:
IS_PUBLIC: "true"
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
- "3.14"
steps:
- uses: actions/checkout@v4
@@ -97,6 +97,33 @@ class AutomationContext:
"""Get the current status message."""
return self._automation_result.status_message
@property
def workspace_id(self) -> Optional[str]:
"""Get the workspace id for the current automation run, if available."""
return self.automation_run_data.workspace_id
def resolve_workspace_id(self) -> Optional[str]:
"""Return workspace id from run data or project lookup fallback."""
workspace_id = self.workspace_id
if workspace_id and workspace_id.strip():
return workspace_id.strip()
project_id = self.automation_run_data.project_id
if not project_id:
return None
try:
project = self.speckle_client.project.get(project_id)
except Exception:
return None
workspace_id = getattr(project, "workspace_id", None)
if isinstance(workspace_id, str) and workspace_id.strip():
resolved_workspace_id = workspace_id.strip()
self.automation_run_data.workspace_id = resolved_workspace_id
return resolved_workspace_id
return None
def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
+2
View File
@@ -68,6 +68,7 @@ def create_test_automation_run(
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
workspaceId
triggers {
payload {
modelId
@@ -119,6 +120,7 @@ def create_test_automation_run_data(
return AutomationRunData(
project_id=test_automation_environment.project_id,
workspace_id=test_automation_run_data.workspace_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data.automation_run_id,
+3 -1
View File
@@ -1,7 +1,7 @@
""""""
from enum import Enum
from typing import Any, Literal
from typing import Any, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
@@ -31,6 +31,7 @@ class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
workspace_id: Optional[str] = None
speckle_server_url: str
automation_id: str
automation_run_id: str
@@ -48,6 +49,7 @@ class TestAutomationRunData(BaseModel):
automation_run_id: str
function_run_id: str
workspace_id: Optional[str] = None
triggers: list[VersionCreationTrigger]
+30 -31
View File
@@ -1,7 +1,7 @@
import contextlib
from dataclasses import dataclass, field
from enum import Enum
from inspect import isclass
from types import UnionType
from typing import (
Any,
ClassVar,
@@ -13,13 +13,11 @@ from typing import (
Tuple,
Type,
Union,
get_origin,
get_type_hints,
)
from warnings import warn
from pydantic.alias_generators import to_pascal
from typing_extensions import get_args
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.memory import MemoryTransport
@@ -222,28 +220,32 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if value in t._value2member_map_:
return True, t(value)
if isinstance(t, ForwardRef):
return True, value
if getattr(t, "__module__", None) == "typing":
if isinstance(t, ForwardRef):
return True, value
if getattr(t, "__module__", None) in ["typing", "types"]:
origin = get_origin(t)
args = get_args(t)
origin = t.__origin__
# below is what in nicer for >= py38
# origin = get_origin(t)
# recursive validation for Unions on both types preferring the fist type
if origin is Union or isinstance(t, UnionType):
if origin is Union:
# below is what in nicer for >= py38
# t_1, t_2 = get_args(t)
args = t.__args__ # type: ignore
for arg_t in args:
ok, v = _validate_type(arg_t, value)
if ok:
return True, v
t_success, t_value = _validate_type(arg_t, value)
if t_success:
return True, t_value
return False, value
if origin is dict:
if not isinstance(value, dict):
return False, value
if not value:
if value == {}:
return True, value
if not args:
if not getattr(t, "__args__", None):
return True, value
t_key, t_value = args
t_key, t_value = t.__args__ # type: ignore
if (
getattr(t_key, "__name__", None),
@@ -263,11 +265,11 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if origin is list:
if not isinstance(value, list):
return False, value
if not value:
if value == []:
return True, value
if not args:
if not hasattr(t, "__args__"):
return True, value
t_items = args[0]
t_items = t.__args__[0] # type: ignore
if getattr(t_items, "__name__", None) == "T":
return True, value
first_item_valid, _ = _validate_type(t_items, value[0])
@@ -278,10 +280,10 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if origin is tuple:
if not isinstance(value, tuple):
return False, value
if not args:
if not hasattr(t, "__args__"):
return True, value
args = t.__args__ # type: ignore
if not args:
if args == tuple():
return True, value
# we're not checking for empty tuple, cause tuple lengths must match
if len(args) != len(value):
@@ -297,7 +299,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if origin is set:
if not isinstance(value, set):
return False, value
if not args:
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
@@ -308,16 +310,13 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if isinstance(value, t):
return True, value
if t is float and type(value) is int:
return True, float(value)
# with contextlib.suppress(ValueError, TypeError):
# if t is float and value is not None:
# return True, float(value)
# # TODO: dafuq, i had to add this not list check
# # but it would also fail for objects and other complex values
# if t is str and value and not isinstance(value, list):
# return True, str(value)
with contextlib.suppress(ValueError, TypeError):
if t is float and value is not None:
return True, float(value)
# TODO: dafuq, i had to add this not list check
# but it would also fail for objects and other complex values
if t is str and value and not isinstance(value, list):
return True, str(value)
return False, value
@@ -264,7 +264,6 @@ class TestIngestionResource:
with pytest.raises(GraphQLException):
_ = client.model_ingestion.fail_with_error(input)
@pytest.mark.skip(reason="TEST FAILS - server behaviour was changed")
def test_complete_non_existent_root_object(
self, client: SpeckleClient, ingestion: ModelIngestion, project: Project
):
+2 -2
View File
@@ -35,7 +35,7 @@ def sample_text_all_properties(sample_point: Point, sample_plane: Plane) -> Text
alignmentH=AlignmentHorizontal.Center,
alignmentV=AlignmentVertical.Center,
plane=sample_plane,
maxWidth=20.0,
maxWidth=20,
units=Units.m,
)
@@ -56,7 +56,7 @@ def test_text_creation_minimal(sample_point: Point):
def test_text_creation_extended(sample_point: Point, sample_plane: Plane):
text_value = "text"
max_width = 20.0
max_width = 20
text_obj = Text(
value=text_value,
+1 -1
View File
@@ -143,7 +143,7 @@ def test_type_checking() -> None:
order = FrozenYoghurt()
order.servings = 2
order.price = 7
order.price = "7" # type: ignore - it will get converted
order.customer = "izzy"
order.dietary = DietaryRestrictions.VEGAN
order.tag = "preorder"
+6 -5
View File
@@ -31,13 +31,13 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
@pytest.mark.parametrize(
"input_type, value, is_valid, return_value",
[
(str, 10, False, 10),
(str, 10, True, "10"),
(str, "foo_bar", True, "foo_bar"),
(
str,
{"foo": "bar"},
False,
{"foo": "bar"},
True,
"{'foo': 'bar'}",
),
(float, 1, True, 1),
# why are we allowing this??? We're lying to our users and ourselves too.
@@ -85,8 +85,9 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
(Dict[int, Base], {1: test_base}, True, {1: test_base}),
(Tuple[int, str, str], (1, "foo", "bar"), True, (1, "foo", "bar")),
(Tuple, (1, "foo", "bar"), True, (1, "foo", "bar")),
(Tuple[str, str, str], (1, "foo", "bar"), False, (1, "foo", "bar")),
(Tuple[str, Optional[str], str], (1, None, "bar"), False, (1, None, "bar")),
# given our current rules, this is the reality. Its just sad...
(Tuple[str, str, str], (1, "foo", "bar"), True, ("1", "foo", "bar")),
(Tuple[str, Optional[str], str], (1, None, "bar"), True, ("1", None, "bar")),
(Set[bool], set([1, 2]), False, set([1, 2])),
(Set[int], set([1, 2]), True, set([1, 2])),
(Set[int], set([None, 2]), True, set([None, 2])),