Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4863a89d8 | |||
| 856f12e57c | |||
| 10e639d19a | |||
| c209cdaec4 | |||
| 169dd00fac | |||
| 58190c378a | |||
| aa16234e7f | |||
| c1f82fa0d2 | |||
| c53a51c8ad | |||
| c1f27b78f9 | |||
| 49d4b7d44d | |||
| 7181f50dda | |||
| 2f84214786 | |||
| 0fe1af8e75 | |||
| 6297943fe1 | |||
| 428bbe2c3d | |||
| 0ca22891bc |
@@ -13,12 +13,14 @@ jobs:
|
|||||||
name: Test (internal)
|
name: Test (internal)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
|
- "3.14"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -62,12 +64,14 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_PUBLIC: "true"
|
IS_PUBLIC: "true"
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.10"
|
- "3.10"
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.12"
|
- "3.12"
|
||||||
- "3.13"
|
- "3.13"
|
||||||
|
- "3.14"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -97,10 +97,7 @@ services:
|
|||||||
|
|
||||||
STRATEGY_LOCAL: "true"
|
STRATEGY_LOCAL: "true"
|
||||||
|
|
||||||
POSTGRES_URL: "postgres"
|
POSTGRES_URL: 'postgres://speckle:speckle@postgres:5432/speckle'
|
||||||
POSTGRES_USER: "speckle"
|
|
||||||
POSTGRES_PASSWORD: "speckle"
|
|
||||||
POSTGRES_DB: "speckle"
|
|
||||||
ENABLE_MP: "false"
|
ENABLE_MP: "false"
|
||||||
|
|
||||||
LOG_PRETTY: "true"
|
LOG_PRETTY: "true"
|
||||||
|
|||||||
@@ -493,29 +493,29 @@ class AutomationContext:
|
|||||||
Args:
|
Args:
|
||||||
level: Result level.
|
level: Result level.
|
||||||
category (str): A short tag for the event type.
|
category (str): A short tag for the event type.
|
||||||
affected_objects (Union[Base, List[Base]]): A single object or a list of
|
affected_objects (Union[Base, List[Base]]): A single object, a list of
|
||||||
objects that are causing the info case.
|
objects, or an empty list. When empty, a result case is still
|
||||||
|
appended with no object IDs (e.g. for skipped rules or version-level
|
||||||
|
messages).
|
||||||
message (Optional[str]): Optional message.
|
message (Optional[str]): Optional message.
|
||||||
metadata: User provided metadata key value pairs
|
metadata: User provided metadata key value pairs
|
||||||
visual_overrides: Case specific 3D visual overrides.
|
visual_overrides: Case specific 3D visual overrides.
|
||||||
"""
|
"""
|
||||||
if isinstance(affected_objects, list):
|
if isinstance(affected_objects, list):
|
||||||
if len(affected_objects) < 1:
|
|
||||||
raise ValueError(
|
|
||||||
f"Need atleast one object to report a(n) {level.value.upper()}"
|
|
||||||
)
|
|
||||||
object_list = affected_objects
|
object_list = affected_objects
|
||||||
else:
|
else:
|
||||||
object_list = [affected_objects]
|
object_list = [affected_objects]
|
||||||
|
|
||||||
ids: Dict[str, Optional[str]] = {}
|
ids: Dict[str, Optional[str]] = {}
|
||||||
|
# When objects are provided, each must have an id (empty list allowed for
|
||||||
|
# version-level/skipped results).
|
||||||
for o in object_list:
|
for o in object_list:
|
||||||
# validate that the Base.id is not None. If its a None, throw an Exception
|
if not getattr(o, "id", None):
|
||||||
if not o.id:
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"You can only attach {level} results to objects with an id."
|
f"You can only attach {level} results to objects with an id."
|
||||||
)
|
)
|
||||||
ids[o.id] = o.applicationId
|
ids[o.id] = getattr(o, "applicationId", None)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
f"Created new {level.value.upper()}"
|
f"Created new {level.value.upper()}"
|
||||||
f" category: {category} caused by: {message}"
|
f" category: {category} caused by: {message}"
|
||||||
|
|||||||
@@ -12,12 +12,23 @@ def data_object_to_speckle(
|
|||||||
step_element: entity_instance,
|
step_element: entity_instance,
|
||||||
children: list[Base],
|
children: list[Base],
|
||||||
current_storey: str | None = None,
|
current_storey: str | None = None,
|
||||||
|
parent_element: entity_instance | None = None,
|
||||||
) -> DataObject:
|
) -> DataObject:
|
||||||
guid = cast(str, step_element.GlobalId)
|
guid = cast(str, step_element.GlobalId)
|
||||||
name = cast(str, step_element.Name or guid)
|
name = cast(str, step_element.Name or guid)
|
||||||
|
|
||||||
properties = extract_properties(step_element)
|
properties = extract_properties(step_element)
|
||||||
|
|
||||||
|
# Add parent ID only if element's parent is also a DataObject (not a Collection)
|
||||||
|
# Collections are: IfcProject and IfcSpatialStructureElement types
|
||||||
|
if (
|
||||||
|
parent_element
|
||||||
|
and hasattr(parent_element, "GlobalId")
|
||||||
|
and not parent_element.is_a("IfcProject")
|
||||||
|
and not parent_element.is_a("IfcSpatialStructureElement")
|
||||||
|
):
|
||||||
|
properties["parentApplicationId"] = parent_element.GlobalId
|
||||||
|
|
||||||
# Add building storey information if available and not a building storey itself
|
# Add building storey information if available and not a building storey itself
|
||||||
if current_storey and not step_element.is_a("IfcBuildingStorey"):
|
if current_storey and not step_element.is_a("IfcBuildingStorey"):
|
||||||
properties["Building Storey"] = current_storey
|
properties["Building Storey"] = current_storey
|
||||||
|
|||||||
@@ -51,4 +51,10 @@ def open_ifc(file_path: str) -> file:
|
|||||||
|
|
||||||
|
|
||||||
def create_geometry_iterator(ifc_file: file | sqlite) -> iterator:
|
def create_geometry_iterator(ifc_file: file | sqlite) -> iterator:
|
||||||
return iterator(_create_iterator_settings(), ifc_file, multiprocessing.cpu_count())
|
GEOMETRY_LIBRARY = "hybrid-opencascade-cgal" # First OCC then fallback to CGAL
|
||||||
|
return iterator(
|
||||||
|
_create_iterator_settings(),
|
||||||
|
ifc_file,
|
||||||
|
multiprocessing.cpu_count(),
|
||||||
|
geometry_library=GEOMETRY_LIBRARY, # type: ignore
|
||||||
|
)
|
||||||
|
|||||||
@@ -44,9 +44,13 @@ class ImportJob:
|
|||||||
_display_value_cache: dict[int, list[Base]] = field(default_factory=dict)
|
_display_value_cache: dict[int, list[Base]] = field(default_factory=dict)
|
||||||
"""Maps an instance step ID to a list of instances"""
|
"""Maps an instance step ID to a list of instances"""
|
||||||
|
|
||||||
def convert_element(self, step_element: entity_instance) -> Base:
|
def convert_element(
|
||||||
|
self,
|
||||||
|
step_element: entity_instance,
|
||||||
|
parent_element: entity_instance | None = None,
|
||||||
|
) -> Base:
|
||||||
try:
|
try:
|
||||||
return self._convert_element(step_element)
|
return self._convert_element(step_element, parent_element)
|
||||||
except SpeckleException:
|
except SpeckleException:
|
||||||
raise
|
raise
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -54,14 +58,18 @@ class ImportJob:
|
|||||||
f"Failed to convert {step_element.is_a()} #{step_element.id()}"
|
f"Failed to convert {step_element.is_a()} #{step_element.id()}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
def _convert_element(self, step_element: entity_instance) -> Base:
|
def _convert_element(
|
||||||
|
self,
|
||||||
|
step_element: entity_instance,
|
||||||
|
parent_element: entity_instance | None = None,
|
||||||
|
) -> Base:
|
||||||
# Track current storey context and store for level proxies
|
# Track current storey context and store for level proxies
|
||||||
previous_storey_data_object = self._current_storey_data_object
|
previous_storey_data_object = self._current_storey_data_object
|
||||||
if step_element.is_a("IfcBuildingStorey"):
|
if step_element.is_a("IfcBuildingStorey"):
|
||||||
# Convert the building storey to a DataObject for the level proxy
|
# Convert the building storey to a DataObject for the level proxy
|
||||||
storey_display_value = self._display_value_cache.get(step_element.id(), [])
|
storey_display_value = self._display_value_cache.get(step_element.id(), [])
|
||||||
self._current_storey_data_object = data_object_to_speckle(
|
self._current_storey_data_object = data_object_to_speckle(
|
||||||
storey_display_value, step_element, []
|
storey_display_value, step_element, [], parent_element=None
|
||||||
)
|
)
|
||||||
|
|
||||||
children = self._convert_children(step_element)
|
children = self._convert_children(step_element)
|
||||||
@@ -86,7 +94,11 @@ class ImportJob:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = data_object_to_speckle(
|
result = data_object_to_speckle(
|
||||||
display_value, step_element, children, current_storey_name
|
display_value,
|
||||||
|
step_element,
|
||||||
|
children,
|
||||||
|
current_storey_name,
|
||||||
|
parent_element,
|
||||||
)
|
)
|
||||||
# Associate non-spatial elements with current storey for level proxies
|
# Associate non-spatial elements with current storey for level proxies
|
||||||
if self._current_storey_data_object is not None and result.applicationId:
|
if self._current_storey_data_object is not None and result.applicationId:
|
||||||
@@ -100,7 +112,7 @@ class ImportJob:
|
|||||||
|
|
||||||
def _convert_children(self, step_element: entity_instance) -> list[Base]:
|
def _convert_children(self, step_element: entity_instance) -> list[Base]:
|
||||||
return [
|
return [
|
||||||
self.convert_element(i)
|
self.convert_element(i, parent_element=step_element)
|
||||||
for i in get_children(step_element)
|
for i in get_children(step_element)
|
||||||
if self._should_convert(i)
|
if self._should_convert(i)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from specklepy.transports.server import ServerTransport
|
|||||||
def open_and_convert_file(
|
def open_and_convert_file(
|
||||||
file_path: str,
|
file_path: str,
|
||||||
project: Project,
|
project: Project,
|
||||||
|
version_message: str,
|
||||||
model_ingestion_id: str,
|
model_ingestion_id: str,
|
||||||
client: SpeckleClient,
|
client: SpeckleClient,
|
||||||
) -> Version:
|
) -> Version:
|
||||||
@@ -86,7 +87,7 @@ def open_and_convert_file(
|
|||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
ingestion_id=model_ingestion_id,
|
ingestion_id=model_ingestion_id,
|
||||||
root_object_id=root_id,
|
root_object_id=root_id,
|
||||||
# version_message=version_message,
|
version_message=version_message,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class ModelIngestionResource(CoreResource):
|
|||||||
metrics.track(metrics.SDK, self.account, {"name": "Ingestion Create"})
|
metrics.track(metrics.SDK, self.account, {"name": "Ingestion Create"})
|
||||||
return super().create(input)
|
return super().create(input)
|
||||||
|
|
||||||
|
def get_ingestion(self, project_id: str, model_ingestion_id: str) -> ModelIngestion:
|
||||||
|
metrics.track(metrics.SDK, self.account, {"name": "Ingestion Get"})
|
||||||
|
return super().get_ingestion(project_id, model_ingestion_id)
|
||||||
|
|
||||||
def update_progress(self, input: ModelIngestionUpdateInput) -> ModelIngestion:
|
def update_progress(self, input: ModelIngestionUpdateInput) -> ModelIngestion:
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "Ingestion Update"})
|
metrics.track(metrics.SDK, self.account, {"name": "Ingestion Update"})
|
||||||
return super().update_progress(input)
|
return super().update_progress(input)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ from specklepy.core.api.inputs.model_inputs import (
|
|||||||
)
|
)
|
||||||
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
||||||
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
|
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
|
||||||
|
from specklepy.core.api.models.current import (
|
||||||
|
ModelPermissionChecks,
|
||||||
|
PermissionCheckResult,
|
||||||
|
)
|
||||||
from specklepy.core.api.resources import ModelResource as CoreResource
|
from specklepy.core.api.resources import ModelResource as CoreResource
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
|
|
||||||
@@ -72,3 +76,17 @@ class ModelResource(CoreResource):
|
|||||||
def update(self, input: UpdateModelInput) -> Model:
|
def update(self, input: UpdateModelInput) -> Model:
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "Model Update"})
|
metrics.track(metrics.SDK, self.account, {"name": "Model Update"})
|
||||||
return super().update(input)
|
return super().update(input)
|
||||||
|
|
||||||
|
def get_permissions(self, model_id: str, project_id: str) -> ModelPermissionChecks:
|
||||||
|
metrics.track(metrics.SDK, self.account, {"name": "Model Get Permissions"})
|
||||||
|
return super().get_permissions(model_id, project_id)
|
||||||
|
|
||||||
|
def can_create_model_ingestion(
|
||||||
|
self, model_id: str, project_id: str
|
||||||
|
) -> PermissionCheckResult:
|
||||||
|
metrics.track(
|
||||||
|
metrics.SDK,
|
||||||
|
self.account,
|
||||||
|
{"name": "Model Get Permissions canCreateIngestion"},
|
||||||
|
)
|
||||||
|
return super().can_create_model_ingestion(model_id, project_id)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class ProjectVisibility(str, Enum):
|
|||||||
PRIVATE = "PRIVATE"
|
PRIVATE = "PRIVATE"
|
||||||
PUBLIC = "PUBLIC"
|
PUBLIC = "PUBLIC"
|
||||||
UNLISTED = "UNLISTED"
|
UNLISTED = "UNLISTED"
|
||||||
|
"""Deprecated, use PUBLIC instead"""
|
||||||
WORKSPACE = "WORKSPACE"
|
WORKSPACE = "WORKSPACE"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class ModelIngestionSuccessInput(GraphQLBaseModel):
|
|||||||
ingestion_id: str
|
ingestion_id: str
|
||||||
project_id: str
|
project_id: str
|
||||||
root_object_id: str
|
root_object_id: str
|
||||||
|
version_message: str | None
|
||||||
|
|
||||||
|
|
||||||
class ModelIngestionFailedInput(GraphQLBaseModel):
|
class ModelIngestionFailedInput(GraphQLBaseModel):
|
||||||
|
|||||||
@@ -34,4 +34,8 @@ class MarkReceivedVersionInput(GraphQLBaseModel):
|
|||||||
version_id: str
|
version_id: str
|
||||||
project_id: str
|
project_id: str
|
||||||
source_application: str
|
source_application: str
|
||||||
|
"""
|
||||||
|
IMPORTANT: this is meant to be the slug of the application that has done the
|
||||||
|
receiving, not to be confused with `Version.sourceApplication`
|
||||||
|
"""
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class PendingStreamCollaborator(GraphQLBaseModel):
|
|||||||
project_name: str
|
project_name: str
|
||||||
title: str
|
title: str
|
||||||
role: str
|
role: str
|
||||||
invited_by: LimitedUser
|
invited_by: LimitedUser | None = None
|
||||||
user: LimitedUser | None = None
|
user: LimitedUser | None = None
|
||||||
token: str | None
|
token: str | None
|
||||||
|
|
||||||
@@ -137,6 +137,12 @@ class Version(GraphQLBaseModel):
|
|||||||
source_application: str | None
|
source_application: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class ModelPermissionChecks(GraphQLBaseModel):
|
||||||
|
can_update: "PermissionCheckResult"
|
||||||
|
can_delete: "PermissionCheckResult"
|
||||||
|
can_create_version: "PermissionCheckResult"
|
||||||
|
|
||||||
|
|
||||||
class Model(GraphQLBaseModel):
|
class Model(GraphQLBaseModel):
|
||||||
author: LimitedUser | None
|
author: LimitedUser | None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -156,7 +162,6 @@ class ProjectPermissionChecks(GraphQLBaseModel):
|
|||||||
can_create_model: "PermissionCheckResult"
|
can_create_model: "PermissionCheckResult"
|
||||||
can_delete: "PermissionCheckResult"
|
can_delete: "PermissionCheckResult"
|
||||||
can_load: "PermissionCheckResult"
|
can_load: "PermissionCheckResult"
|
||||||
can_publish: "PermissionCheckResult"
|
|
||||||
|
|
||||||
|
|
||||||
class Project(GraphQLBaseModel):
|
class Project(GraphQLBaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Optional, Tuple
|
from typing import Any, Tuple
|
||||||
|
|
||||||
from gql import Client, gql
|
from gql import Client, gql
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class ModelIngestionResource(ResourceBase):
|
|||||||
account: Account,
|
account: Account,
|
||||||
basepath: str,
|
basepath: str,
|
||||||
client: Client,
|
client: Client,
|
||||||
server_version: Optional[Tuple[Any, ...]],
|
server_version: Tuple[Any, ...] | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
account=account,
|
account=account,
|
||||||
@@ -40,24 +40,23 @@ class ModelIngestionResource(ResourceBase):
|
|||||||
server_version=server_version,
|
server_version=server_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_ingestion(self, project_id: str, model_id: str) -> ModelIngestion:
|
def get_ingestion(self, project_id: str, model_ingestion_id: str) -> ModelIngestion:
|
||||||
QUERY = gql(
|
QUERY = gql(
|
||||||
"""
|
"""
|
||||||
query Query($projectId: String!, $modelId: String!) {
|
query Query($projectId: String!, $modelIngestionId: ID!) {
|
||||||
data:project(id: $projectId) {
|
data:project(id: $projectId) {
|
||||||
data:model(id: $modelId) {
|
data:ingestion(id: $modelIngestionId) {
|
||||||
data:ingestion {
|
id
|
||||||
id
|
createdAt
|
||||||
createdAt
|
updatedAt
|
||||||
modelId
|
modelId
|
||||||
cancellationRequested
|
cancellationRequested
|
||||||
statusData {
|
statusData {
|
||||||
... on HasModelIngestionStatus {
|
... on HasModelIngestionStatus {
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
... on HasProgressMessage {
|
... on HasProgressMessage {
|
||||||
progressMessage
|
progressMessage
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,14 +67,14 @@ class ModelIngestionResource(ResourceBase):
|
|||||||
|
|
||||||
variables = {
|
variables = {
|
||||||
"projectId": project_id,
|
"projectId": project_id,
|
||||||
"modelId": model_id,
|
"modelIngestionId": model_ingestion_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.make_request_and_parse_response(
|
return self.make_request_and_parse_response(
|
||||||
DataResponse[DataResponse[DataResponse[ModelIngestion]]],
|
DataResponse[DataResponse[ModelIngestion]],
|
||||||
QUERY,
|
QUERY,
|
||||||
variables,
|
variables,
|
||||||
).data.data.data
|
).data.data
|
||||||
|
|
||||||
def create(self, input: ModelIngestionCreateInput) -> ModelIngestion:
|
def create(self, input: ModelIngestionCreateInput) -> ModelIngestion:
|
||||||
QUERY = gql(
|
QUERY = gql(
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ from specklepy.core.api.inputs.model_inputs import (
|
|||||||
)
|
)
|
||||||
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
||||||
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
|
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
|
||||||
|
from specklepy.core.api.models.current import (
|
||||||
|
ModelPermissionChecks,
|
||||||
|
PermissionCheckResult,
|
||||||
|
)
|
||||||
from specklepy.core.api.resource import ResourceBase
|
from specklepy.core.api.resource import ResourceBase
|
||||||
from specklepy.core.api.responses import DataResponse
|
from specklepy.core.api.responses import DataResponse
|
||||||
|
|
||||||
@@ -299,3 +303,71 @@ class ModelResource(ResourceBase):
|
|||||||
return self.make_request_and_parse_response(
|
return self.make_request_and_parse_response(
|
||||||
DataResponse[DataResponse[Model]], QUERY, variables
|
DataResponse[DataResponse[Model]], QUERY, variables
|
||||||
).data.data
|
).data.data
|
||||||
|
|
||||||
|
def get_permissions(self, project_id: str, model_id: str) -> ModelPermissionChecks:
|
||||||
|
QUERY = gql(
|
||||||
|
"""
|
||||||
|
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||||
|
data:project(id: $projectId) {
|
||||||
|
data:model(id: $modelId) {
|
||||||
|
data:permissions {
|
||||||
|
canUpdate {
|
||||||
|
authorized
|
||||||
|
code
|
||||||
|
message
|
||||||
|
}
|
||||||
|
canDelete {
|
||||||
|
authorized
|
||||||
|
code
|
||||||
|
message
|
||||||
|
}
|
||||||
|
canCreateVersion {
|
||||||
|
authorized
|
||||||
|
code
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
variables = {"projectId": project_id, "modelId": model_id}
|
||||||
|
|
||||||
|
return self.make_request_and_parse_response(
|
||||||
|
DataResponse[DataResponse[DataResponse[ModelPermissionChecks]]],
|
||||||
|
QUERY,
|
||||||
|
variables,
|
||||||
|
).data.data.data
|
||||||
|
|
||||||
|
def can_create_model_ingestion(
|
||||||
|
self, project_id: str, model_id: str
|
||||||
|
) -> PermissionCheckResult:
|
||||||
|
QUERY = gql(
|
||||||
|
"""
|
||||||
|
query ModelPermissions($projectId: String!, $modelId: String!) {
|
||||||
|
data:project(id: $projectId) {
|
||||||
|
data:model(id: $modelId) {
|
||||||
|
data:permissions {
|
||||||
|
data:canCreateIngestion {
|
||||||
|
authorized
|
||||||
|
code
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
variables = {"projectId": project_id, "modelId": model_id}
|
||||||
|
|
||||||
|
return self.make_request_and_parse_response(
|
||||||
|
DataResponse[
|
||||||
|
DataResponse[DataResponse[DataResponse[PermissionCheckResult]]]
|
||||||
|
],
|
||||||
|
QUERY,
|
||||||
|
variables,
|
||||||
|
).data.data.data.data
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import contextlib
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
|
from types import UnionType
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
ClassVar,
|
ClassVar,
|
||||||
@@ -13,11 +13,13 @@ from typing import (
|
|||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
|
get_origin,
|
||||||
get_type_hints,
|
get_type_hints,
|
||||||
)
|
)
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from pydantic.alias_generators import to_pascal
|
from pydantic.alias_generators import to_pascal
|
||||||
|
from typing_extensions import get_args
|
||||||
|
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from specklepy.transports.memory import MemoryTransport
|
from specklepy.transports.memory import MemoryTransport
|
||||||
@@ -220,32 +222,28 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
|||||||
if value in t._value2member_map_:
|
if value in t._value2member_map_:
|
||||||
return True, t(value)
|
return True, t(value)
|
||||||
|
|
||||||
if getattr(t, "__module__", None) == "typing":
|
if isinstance(t, ForwardRef):
|
||||||
if isinstance(t, ForwardRef):
|
return True, value
|
||||||
return True, value
|
|
||||||
|
|
||||||
origin = t.__origin__
|
if getattr(t, "__module__", None) in ["typing", "types"]:
|
||||||
# below is what in nicer for >= py38
|
origin = get_origin(t)
|
||||||
# origin = get_origin(t)
|
args = get_args(t)
|
||||||
|
|
||||||
# recursive validation for Unions on both types preferring the fist type
|
# recursive validation for Unions on both types preferring the fist type
|
||||||
if origin is Union:
|
if origin is Union or isinstance(t, UnionType):
|
||||||
# below is what in nicer for >= py38
|
|
||||||
# t_1, t_2 = get_args(t)
|
|
||||||
args = t.__args__ # type: ignore
|
|
||||||
for arg_t in args:
|
for arg_t in args:
|
||||||
t_success, t_value = _validate_type(arg_t, value)
|
ok, v = _validate_type(arg_t, value)
|
||||||
if t_success:
|
if ok:
|
||||||
return True, t_value
|
return True, v
|
||||||
return False, value
|
return False, value
|
||||||
if origin is dict:
|
if origin is dict:
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
return False, value
|
return False, value
|
||||||
if value == {}:
|
if not value:
|
||||||
return True, value
|
return True, value
|
||||||
if not getattr(t, "__args__", None):
|
if not args:
|
||||||
return True, value
|
return True, value
|
||||||
t_key, t_value = t.__args__ # type: ignore
|
t_key, t_value = args
|
||||||
|
|
||||||
if (
|
if (
|
||||||
getattr(t_key, "__name__", None),
|
getattr(t_key, "__name__", None),
|
||||||
@@ -265,11 +263,11 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
|||||||
if origin is list:
|
if origin is list:
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
return False, value
|
return False, value
|
||||||
if value == []:
|
if not value:
|
||||||
return True, value
|
return True, value
|
||||||
if not hasattr(t, "__args__"):
|
if not args:
|
||||||
return True, value
|
return True, value
|
||||||
t_items = t.__args__[0] # type: ignore
|
t_items = args[0]
|
||||||
if getattr(t_items, "__name__", None) == "T":
|
if getattr(t_items, "__name__", None) == "T":
|
||||||
return True, value
|
return True, value
|
||||||
first_item_valid, _ = _validate_type(t_items, value[0])
|
first_item_valid, _ = _validate_type(t_items, value[0])
|
||||||
@@ -280,10 +278,10 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
|||||||
if origin is tuple:
|
if origin is tuple:
|
||||||
if not isinstance(value, tuple):
|
if not isinstance(value, tuple):
|
||||||
return False, value
|
return False, value
|
||||||
if not hasattr(t, "__args__"):
|
if not args:
|
||||||
return True, value
|
return True, value
|
||||||
args = t.__args__ # type: ignore
|
args = t.__args__ # type: ignore
|
||||||
if args == tuple():
|
if not args:
|
||||||
return True, value
|
return True, value
|
||||||
# we're not checking for empty tuple, cause tuple lengths must match
|
# we're not checking for empty tuple, cause tuple lengths must match
|
||||||
if len(args) != len(value):
|
if len(args) != len(value):
|
||||||
@@ -299,7 +297,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
|||||||
if origin is set:
|
if origin is set:
|
||||||
if not isinstance(value, set):
|
if not isinstance(value, set):
|
||||||
return False, value
|
return False, value
|
||||||
if not hasattr(t, "__args__"):
|
if not args:
|
||||||
return True, value
|
return True, value
|
||||||
t_items = t.__args__[0] # type: ignore
|
t_items = t.__args__[0] # type: ignore
|
||||||
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
|
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
|
||||||
@@ -310,13 +308,16 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
|||||||
if isinstance(value, t):
|
if isinstance(value, t):
|
||||||
return True, value
|
return True, value
|
||||||
|
|
||||||
with contextlib.suppress(ValueError, TypeError):
|
if t is float and type(value) is int:
|
||||||
if t is float and value is not None:
|
return True, float(value)
|
||||||
return True, float(value)
|
|
||||||
# TODO: dafuq, i had to add this not list check
|
# with contextlib.suppress(ValueError, TypeError):
|
||||||
# but it would also fail for objects and other complex values
|
# if t is float and value is not None:
|
||||||
if t is str and value and not isinstance(value, list):
|
# return True, float(value)
|
||||||
return True, str(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
|
return False, value
|
||||||
|
|
||||||
|
|||||||
@@ -50,12 +50,10 @@ class TestActiveUserResourcePermissions:
|
|||||||
assert hasattr(permissions, "can_create_model")
|
assert hasattr(permissions, "can_create_model")
|
||||||
assert hasattr(permissions, "can_delete")
|
assert hasattr(permissions, "can_delete")
|
||||||
assert hasattr(permissions, "can_load")
|
assert hasattr(permissions, "can_load")
|
||||||
assert hasattr(permissions, "can_publish")
|
|
||||||
|
|
||||||
assert permissions.can_create_model.authorized is True
|
assert permissions.can_create_model.authorized is True
|
||||||
assert permissions.can_delete.authorized is True
|
assert permissions.can_delete.authorized is True
|
||||||
assert permissions.can_load.authorized is True
|
assert permissions.can_load.authorized is True
|
||||||
assert permissions.can_publish.authorized is True
|
|
||||||
|
|
||||||
def test_active_user_get_projects_with_permissions_with_filter(
|
def test_active_user_get_projects_with_permissions_with_filter(
|
||||||
self, client: SpeckleClient, test_project: Project
|
self, client: SpeckleClient, test_project: Project
|
||||||
|
|||||||
@@ -79,6 +79,15 @@ class TestIngestionResource:
|
|||||||
|
|
||||||
return ingestion
|
return ingestion
|
||||||
|
|
||||||
|
def test_get_ingestion(
|
||||||
|
self, client: SpeckleClient, project: Project, ingestion: ModelIngestion
|
||||||
|
):
|
||||||
|
queried_ingestion = client.model_ingestion.get_ingestion(
|
||||||
|
project.id, ingestion.id
|
||||||
|
)
|
||||||
|
assert queried_ingestion.id == ingestion.id
|
||||||
|
assert queried_ingestion.status_data.status == ingestion.status_data.status
|
||||||
|
|
||||||
def test_update_progress(
|
def test_update_progress(
|
||||||
self, client: SpeckleClient, ingestion: ModelIngestion, project: Project
|
self, client: SpeckleClient, ingestion: ModelIngestion, project: Project
|
||||||
):
|
):
|
||||||
@@ -198,7 +207,7 @@ class TestIngestionResource:
|
|||||||
ingestion_id=ingestion.id,
|
ingestion_id=ingestion.id,
|
||||||
root_object_id=object_id,
|
root_object_id=object_id,
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
# version_message=None,
|
version_message=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
res = client.model_ingestion.complete(input)
|
res = client.model_ingestion.complete(input)
|
||||||
@@ -255,6 +264,7 @@ class TestIngestionResource:
|
|||||||
with pytest.raises(GraphQLException):
|
with pytest.raises(GraphQLException):
|
||||||
_ = client.model_ingestion.fail_with_error(input)
|
_ = client.model_ingestion.fail_with_error(input)
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason="TEST FAILS - server behaviour was changed")
|
||||||
def test_complete_non_existent_root_object(
|
def test_complete_non_existent_root_object(
|
||||||
self, client: SpeckleClient, ingestion: ModelIngestion, project: Project
|
self, client: SpeckleClient, ingestion: ModelIngestion, project: Project
|
||||||
):
|
):
|
||||||
@@ -262,7 +272,7 @@ class TestIngestionResource:
|
|||||||
ingestion_id=ingestion.id,
|
ingestion_id=ingestion.id,
|
||||||
root_object_id="asdfasdfasdfasfd",
|
root_object_id="asdfasdfasdfasfd",
|
||||||
project_id=project.id,
|
project_id=project.id,
|
||||||
# version_message=None,
|
version_message=None,
|
||||||
)
|
)
|
||||||
with pytest.raises(GraphQLException):
|
with pytest.raises(GraphQLException):
|
||||||
_ = client.model_ingestion.complete(input)
|
_ = client.model_ingestion.complete(input)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from specklepy.api.client import SpeckleClient
|
from specklepy.api.client import SpeckleClient
|
||||||
|
from specklepy.core.api.enums import ProjectVisibility
|
||||||
from specklepy.core.api.inputs.model_inputs import (
|
from specklepy.core.api.inputs.model_inputs import (
|
||||||
CreateModelInput,
|
CreateModelInput,
|
||||||
DeleteModelInput,
|
DeleteModelInput,
|
||||||
@@ -12,11 +13,14 @@ from specklepy.core.api.inputs.project_inputs import (
|
|||||||
)
|
)
|
||||||
from specklepy.core.api.models.current import (
|
from specklepy.core.api.models.current import (
|
||||||
Model,
|
Model,
|
||||||
|
ModelPermissionChecks,
|
||||||
|
PermissionCheckResult,
|
||||||
Project,
|
Project,
|
||||||
ProjectWithModels,
|
ProjectWithModels,
|
||||||
ResourceCollection,
|
ResourceCollection,
|
||||||
)
|
)
|
||||||
from specklepy.logging.exceptions import GraphQLException
|
from specklepy.logging.exceptions import GraphQLException
|
||||||
|
from tests.integration.conftest import is_internal, is_public
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.run()
|
@pytest.mark.run()
|
||||||
@@ -24,7 +28,9 @@ class TestModelResource:
|
|||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def test_project(self, client: SpeckleClient) -> Project:
|
def test_project(self, client: SpeckleClient) -> Project:
|
||||||
project = client.project.create(
|
project = client.project.create(
|
||||||
ProjectCreateInput(name="Test project", description="", visibility=None)
|
ProjectCreateInput(
|
||||||
|
name="Test project", description="", visibility=ProjectVisibility.PUBLIC
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
@@ -149,3 +155,52 @@ class TestModelResource:
|
|||||||
|
|
||||||
with pytest.raises(GraphQLException):
|
with pytest.raises(GraphQLException):
|
||||||
client.model.delete(delete_data)
|
client.model.delete(delete_data)
|
||||||
|
|
||||||
|
def test_model_get_permissions(
|
||||||
|
self,
|
||||||
|
client: SpeckleClient,
|
||||||
|
second_client: SpeckleClient,
|
||||||
|
test_project: Project,
|
||||||
|
test_model: Model,
|
||||||
|
):
|
||||||
|
result = client.model.get_permissions(test_project.id, test_model.id)
|
||||||
|
|
||||||
|
assert isinstance(result, ModelPermissionChecks)
|
||||||
|
assert result.can_update.authorized is True
|
||||||
|
assert result.can_create_version.authorized is True
|
||||||
|
assert result.can_delete.authorized is True
|
||||||
|
|
||||||
|
guest = second_client.model.get_permissions(test_project.id, test_model.id)
|
||||||
|
|
||||||
|
assert isinstance(guest, ModelPermissionChecks)
|
||||||
|
assert guest.can_update.authorized is False
|
||||||
|
assert guest.can_create_version.authorized is False
|
||||||
|
assert guest.can_delete.authorized is False
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
is_public(), reason="API only available on server versions 3.0.11 or greater"
|
||||||
|
)
|
||||||
|
def test_can_create_model_ingestion_internal_server(
|
||||||
|
self,
|
||||||
|
client: SpeckleClient,
|
||||||
|
test_project: Project,
|
||||||
|
test_model: Model,
|
||||||
|
):
|
||||||
|
result = client.model.can_create_model_ingestion(test_project.id, test_model.id)
|
||||||
|
|
||||||
|
assert isinstance(result, PermissionCheckResult)
|
||||||
|
assert result.authorized is True
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
is_internal(),
|
||||||
|
reason="API only available on server versions 3.0.11 or greater",
|
||||||
|
)
|
||||||
|
def test_can_create_model_ingestion_public_server(
|
||||||
|
self,
|
||||||
|
client: SpeckleClient,
|
||||||
|
test_project: Project,
|
||||||
|
test_model: Model,
|
||||||
|
):
|
||||||
|
with pytest.raises(GraphQLException) as ex:
|
||||||
|
_ = client.model.can_create_model_ingestion(test_project.id, test_model.id)
|
||||||
|
assert "GRAPHQL_VALIDATION_FAILED" in str(ex.value)
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ class TestProjectResource:
|
|||||||
)
|
)
|
||||||
return project
|
return project
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def test_public_project(self, client: SpeckleClient) -> Project:
|
||||||
|
project = client.project.create(
|
||||||
|
ProjectCreateInput(
|
||||||
|
name="test project123",
|
||||||
|
description="desc",
|
||||||
|
visibility=ProjectVisibility.PUBLIC,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return project
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"name, description, visibility",
|
"name, description, visibility",
|
||||||
[
|
[
|
||||||
@@ -50,7 +61,7 @@ class TestProjectResource:
|
|||||||
assert result.id is not None
|
assert result.id is not None
|
||||||
assert result.name == name
|
assert result.name == name
|
||||||
assert result.description == (description or "")
|
assert result.description == (description or "")
|
||||||
# we've disabled creation of public projects for now, they fall back to unlisted
|
# we've disabled creation of unlisted projects for now, they fall back to public
|
||||||
if visibility == ProjectVisibility.UNLISTED:
|
if visibility == ProjectVisibility.UNLISTED:
|
||||||
assert result.visibility == ProjectVisibility.PUBLIC
|
assert result.visibility == ProjectVisibility.PUBLIC
|
||||||
else:
|
else:
|
||||||
@@ -67,13 +78,32 @@ class TestProjectResource:
|
|||||||
assert result.created_at == test_project.created_at
|
assert result.created_at == test_project.created_at
|
||||||
|
|
||||||
def test_project_get_permissions(
|
def test_project_get_permissions(
|
||||||
self, client: SpeckleClient, test_project: Project
|
self,
|
||||||
|
client: SpeckleClient,
|
||||||
|
second_client: SpeckleClient,
|
||||||
|
test_project: Project,
|
||||||
|
test_public_project: Project,
|
||||||
):
|
):
|
||||||
result = client.project.get_permissions(test_project.id)
|
result_private = client.project.get_permissions(test_project.id)
|
||||||
|
|
||||||
|
assert isinstance(result_private, ProjectPermissionChecks)
|
||||||
|
assert result_private.can_create_model.authorized is True
|
||||||
|
assert result_private.can_delete.authorized is True
|
||||||
|
assert result_private.can_load.authorized is True
|
||||||
|
|
||||||
|
result = client.project.get_permissions(test_public_project.id)
|
||||||
|
|
||||||
assert isinstance(result, ProjectPermissionChecks)
|
assert isinstance(result, ProjectPermissionChecks)
|
||||||
assert result.can_create_model.authorized is True
|
assert result.can_create_model.authorized is True
|
||||||
assert result.can_delete.authorized is True
|
assert result.can_delete.authorized is True
|
||||||
|
assert result.can_load.authorized is True
|
||||||
|
|
||||||
|
guest = second_client.project.get_permissions(test_public_project.id)
|
||||||
|
|
||||||
|
assert isinstance(result, ProjectPermissionChecks)
|
||||||
|
assert guest.can_create_model.authorized is False
|
||||||
|
assert guest.can_delete.authorized is False
|
||||||
|
assert guest.can_load.authorized is False
|
||||||
|
|
||||||
def test_project_update(self, client: SpeckleClient, test_project: Project):
|
def test_project_update(self, client: SpeckleClient, test_project: Project):
|
||||||
new_name = "MY new name"
|
new_name = "MY new name"
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ def is_public() -> bool:
|
|||||||
return os.getenv("IS_PUBLIC", "false").lower() == "true"
|
return os.getenv("IS_PUBLIC", "false").lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def is_internal() -> bool:
|
||||||
|
return not is_public()
|
||||||
|
|
||||||
|
|
||||||
def seed_user(host: str) -> Dict[str, str]:
|
def seed_user(host: str) -> Dict[str, str]:
|
||||||
seed = uuid.uuid4().hex
|
seed = uuid.uuid4().hex
|
||||||
user_dict = {
|
user_dict = {
|
||||||
|
|||||||
@@ -157,10 +157,10 @@ def test_parse_project():
|
|||||||
|
|
||||||
def test_parse_model():
|
def test_parse_model():
|
||||||
wrap = StreamWrapper(
|
wrap = StreamWrapper(
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/d9eb4918c8"
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert wrap.branch_name == "building wrapper"
|
assert wrap.branch_name == "speckle tower revit 2025"
|
||||||
assert wrap.type == "branch"
|
assert wrap.type == "branch"
|
||||||
|
|
||||||
|
|
||||||
@@ -191,10 +191,10 @@ def test_parse_object_fe2():
|
|||||||
|
|
||||||
def test_parse_version():
|
def test_parse_version():
|
||||||
wrap = StreamWrapper(
|
wrap = StreamWrapper(
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1"
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d@7199443eff"
|
||||||
)
|
)
|
||||||
wrap_quoted = StreamWrapper(
|
wrap_quoted = StreamWrapper(
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1"
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d%407199443eff"
|
||||||
)
|
)
|
||||||
assert wrap.type == "commit"
|
assert wrap.type == "commit"
|
||||||
assert wrap_quoted.type == "commit"
|
assert wrap_quoted.type == "commit"
|
||||||
@@ -208,11 +208,11 @@ def test_to_string():
|
|||||||
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893",
|
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893",
|
||||||
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792",
|
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792",
|
||||||
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6",
|
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6",
|
||||||
"https://latest.speckle.systems/projects/843d07eb10",
|
"https://app.speckle.systems/projects/8be1007be1",
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838",
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d",
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1",
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d@7199443eff",
|
||||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1",
|
"https://app.speckle.systems/projects/8be1007be1/models/cc7578012d%407199443eff",
|
||||||
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c",
|
"https://app.speckle.systems/projects/8be1007be1/models/9b5e57dca804a923a8d42d55dcc0191a",
|
||||||
]
|
]
|
||||||
for url in urls:
|
for url in urls:
|
||||||
wrap = StreamWrapper(url)
|
wrap = StreamWrapper(url)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def sample_text_all_properties(sample_point: Point, sample_plane: Plane) -> Text
|
|||||||
alignmentH=AlignmentHorizontal.Center,
|
alignmentH=AlignmentHorizontal.Center,
|
||||||
alignmentV=AlignmentVertical.Center,
|
alignmentV=AlignmentVertical.Center,
|
||||||
plane=sample_plane,
|
plane=sample_plane,
|
||||||
maxWidth=20,
|
maxWidth=20.0,
|
||||||
units=Units.m,
|
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):
|
def test_text_creation_extended(sample_point: Point, sample_plane: Plane):
|
||||||
text_value = "text"
|
text_value = "text"
|
||||||
max_width = 20
|
max_width = 20.0
|
||||||
|
|
||||||
text_obj = Text(
|
text_obj = Text(
|
||||||
value=text_value,
|
value=text_value,
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
"""Unit tests for AutomationContext.attach_result_to_objects contract."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from speckle_automate import AutomationContext
|
||||||
|
from speckle_automate.schema import (
|
||||||
|
AutomationRunData,
|
||||||
|
ObjectResultLevel,
|
||||||
|
VersionCreationTrigger,
|
||||||
|
VersionCreationTriggerPayload,
|
||||||
|
)
|
||||||
|
from specklepy.objects.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
def _minimal_automation_context() -> AutomationContext:
|
||||||
|
run_data = AutomationRunData(
|
||||||
|
project_id="p",
|
||||||
|
speckle_server_url="http://localhost",
|
||||||
|
automation_id="a",
|
||||||
|
automation_run_id="r",
|
||||||
|
function_run_id="f",
|
||||||
|
triggers=[
|
||||||
|
VersionCreationTrigger(
|
||||||
|
trigger_type="versionCreation",
|
||||||
|
payload=VersionCreationTriggerPayload(model_id="m", version_id="v"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return AutomationContext(
|
||||||
|
automation_run_data=run_data,
|
||||||
|
speckle_client=MagicMock(),
|
||||||
|
_server_transport=MagicMock(),
|
||||||
|
_speckle_token="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_attach_result_to_objects_accepts_empty_list() -> None:
|
||||||
|
"""Empty affected_objects appends one result case with no object IDs."""
|
||||||
|
ctx = _minimal_automation_context()
|
||||||
|
assert len(ctx._automation_result.object_results) == 0
|
||||||
|
|
||||||
|
ctx.attach_result_to_objects(
|
||||||
|
ObjectResultLevel.WARNING,
|
||||||
|
"SkippedRule",
|
||||||
|
[],
|
||||||
|
message="No elements to check.",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(ctx._automation_result.object_results) == 1
|
||||||
|
case = ctx._automation_result.object_results[0]
|
||||||
|
assert case.level == ObjectResultLevel.WARNING
|
||||||
|
assert case.category == "SkippedRule"
|
||||||
|
assert case.object_app_ids == {}
|
||||||
|
assert case.message == "No elements to check."
|
||||||
|
|
||||||
|
|
||||||
|
def test_attach_result_to_objects_with_objects_appends_case_with_ids() -> None:
|
||||||
|
"""Single or multiple objects with id produce result case with object_app_ids."""
|
||||||
|
ctx = _minimal_automation_context()
|
||||||
|
obj1 = Base()
|
||||||
|
obj1.id = "id-one"
|
||||||
|
obj1.applicationId = "app-one"
|
||||||
|
obj2 = Base()
|
||||||
|
obj2.id = "id-two"
|
||||||
|
|
||||||
|
ctx.attach_result_to_objects(
|
||||||
|
ObjectResultLevel.ERROR,
|
||||||
|
"BadType",
|
||||||
|
[obj1, obj2],
|
||||||
|
message="Invalid type.",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(ctx._automation_result.object_results) == 1
|
||||||
|
case = ctx._automation_result.object_results[0]
|
||||||
|
assert case.level == ObjectResultLevel.ERROR
|
||||||
|
assert case.category == "BadType"
|
||||||
|
assert case.object_app_ids == {"id-one": "app-one", "id-two": None}
|
||||||
|
assert case.message == "Invalid type."
|
||||||
|
|
||||||
|
|
||||||
|
def test_attach_result_to_objects_raises_when_object_has_no_id() -> None:
|
||||||
|
"""At least one object without id raises."""
|
||||||
|
ctx = _minimal_automation_context()
|
||||||
|
obj = Base()
|
||||||
|
obj.id = None
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="results to objects with an id"):
|
||||||
|
ctx.attach_result_to_objects(
|
||||||
|
ObjectResultLevel.ERROR,
|
||||||
|
"Bad",
|
||||||
|
obj,
|
||||||
|
message="No id.",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(ctx._automation_result.object_results) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_attach_info_to_objects_accepts_empty_list() -> None:
|
||||||
|
"""attach_info_to_objects (convenience method) also accepts empty list."""
|
||||||
|
ctx = _minimal_automation_context()
|
||||||
|
|
||||||
|
ctx.attach_info_to_objects("VersionLevel", [], message="No levels in model.")
|
||||||
|
|
||||||
|
assert len(ctx._automation_result.object_results) == 1
|
||||||
|
case = ctx._automation_result.object_results[0]
|
||||||
|
assert case.level == ObjectResultLevel.INFO
|
||||||
|
assert case.category == "VersionLevel"
|
||||||
|
assert case.object_app_ids == {}
|
||||||
@@ -143,7 +143,7 @@ def test_type_checking() -> None:
|
|||||||
order = FrozenYoghurt()
|
order = FrozenYoghurt()
|
||||||
|
|
||||||
order.servings = 2
|
order.servings = 2
|
||||||
order.price = "7" # type: ignore - it will get converted
|
order.price = 7
|
||||||
order.customer = "izzy"
|
order.customer = "izzy"
|
||||||
order.dietary = DietaryRestrictions.VEGAN
|
order.dietary = DietaryRestrictions.VEGAN
|
||||||
order.tag = "preorder"
|
order.tag = "preorder"
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"input_type, value, is_valid, return_value",
|
"input_type, value, is_valid, return_value",
|
||||||
[
|
[
|
||||||
(str, 10, True, "10"),
|
(str, 10, False, 10),
|
||||||
(str, "foo_bar", True, "foo_bar"),
|
(str, "foo_bar", True, "foo_bar"),
|
||||||
(
|
(
|
||||||
str,
|
str,
|
||||||
{"foo": "bar"},
|
{"foo": "bar"},
|
||||||
True,
|
False,
|
||||||
"{'foo': 'bar'}",
|
{"foo": "bar"},
|
||||||
),
|
),
|
||||||
(float, 1, True, 1),
|
(float, 1, True, 1),
|
||||||
# why are we allowing this??? We're lying to our users and ourselves too.
|
# why are we allowing this??? We're lying to our users and ourselves too.
|
||||||
@@ -85,9 +85,8 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
|
|||||||
(Dict[int, Base], {1: test_base}, True, {1: test_base}),
|
(Dict[int, Base], {1: test_base}, True, {1: test_base}),
|
||||||
(Tuple[int, str, str], (1, "foo", "bar"), True, (1, "foo", "bar")),
|
(Tuple[int, str, str], (1, "foo", "bar"), True, (1, "foo", "bar")),
|
||||||
(Tuple, (1, "foo", "bar"), True, (1, "foo", "bar")),
|
(Tuple, (1, "foo", "bar"), True, (1, "foo", "bar")),
|
||||||
# given our current rules, this is the reality. Its just sad...
|
(Tuple[str, str, str], (1, "foo", "bar"), False, (1, "foo", "bar")),
|
||||||
(Tuple[str, str, str], (1, "foo", "bar"), True, ("1", "foo", "bar")),
|
(Tuple[str, Optional[str], str], (1, None, "bar"), False, (1, None, "bar")),
|
||||||
(Tuple[str, Optional[str], str], (1, None, "bar"), True, ("1", None, "bar")),
|
|
||||||
(Set[bool], set([1, 2]), False, set([1, 2])),
|
(Set[bool], set([1, 2]), False, set([1, 2])),
|
||||||
(Set[int], set([1, 2]), True, set([1, 2])),
|
(Set[int], set([1, 2]), True, set([1, 2])),
|
||||||
(Set[int], set([None, 2]), True, set([None, 2])),
|
(Set[int], set([None, 2]), True, set([None, 2])),
|
||||||
|
|||||||
Reference in New Issue
Block a user