Skip to content

Commit

Permalink
Merge pull request #305 from specklesystems/gergo/automation_runner_r…
Browse files Browse the repository at this point in the history
…efactor

gergo/automation runner refactor
  • Loading branch information
gjedlicska authored Oct 11, 2023
2 parents 9dba99a + 6a6b3d4 commit 52c8e37
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 140 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: true
docker_layer_caching: false
resource_class: medium
parameters:
tag:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.9.1"
version = "2.17.5"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <[email protected]>"]
Expand Down
4 changes: 2 additions & 2 deletions src/speckle_automate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResult,
ObjectResultLevel,
ResultCase,
)

__all__ = [
Expand All @@ -16,7 +16,7 @@
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ObjectResult",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
Expand Down
150 changes: 125 additions & 25 deletions src/speckle_automate/automation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass, field
from pathlib import Path
import time
from typing import Optional, Union
from typing import Any, Dict, List, Optional, Union

import httpx
from gql import gql
Expand All @@ -19,8 +19,8 @@
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResult,
ObjectResultLevel,
ResultCase,
)


Expand Down Expand Up @@ -84,9 +84,14 @@ def run_status(self) -> AutomationStatus:
"""Get the status of the automation run."""
return self._automation_result.run_status

@property
def status_message(self) -> Optional[str]:
"""Get the current status message."""
return self._automation_result.status_message

def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
return (time.perf_counter() - self._init_time) / 1000

def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
Expand All @@ -99,21 +104,22 @@ def receive_version(self) -> Base:
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():2f} seconds to receive",
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {self.automation_run_data.version_id}",
)
return base

def create_new_version_in_project(
self, root_object: Base, model_id: str, version_message: str = ""
) -> None:
) -> str:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""

if model_id == self.automation_run_data.model_id:
raise ValueError(
f"The target model id: {model_id} cannot match the model id"
Expand Down Expand Up @@ -142,6 +148,25 @@ def create_new_version_in_project(
raise version_id

self._automation_result.result_versions.append(version_id)
return version_id

def _get_model(self, model_id: str) -> Branch:
query = gql(
"""
query ProjectModel($projectId: String!, $modelId: String!){
project(id: $projectId) {
model(id: $modelId) {
name
id
description
}
}
}
"""
)
params = {"projectId": self.automation_run_data.project_id, "modelId": model_id}
response = self.speckle_client.httpclient.execute(query, params)
return Branch.model_validate(response["project"]["model"])

def _get_model(self, model_id: str) -> Branch:
query = gql(
Expand Down Expand Up @@ -201,14 +226,15 @@ def report_run_status(self) -> None:
object_results = {
"version": "1.0.0",
"values": {
"speckleObjects": self._automation_result.model_dump(by_alias=True)[
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobs": self._automation_result.blobs,
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None

params = {
"automationId": self.automation_run_data.automation_id,
"automationRevisionId": self.automation_run_data.automation_revision_id,
Expand Down Expand Up @@ -260,8 +286,9 @@ class BlobUploadResponse(AutomateBase):
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")

for upload_result in upload_response.upload_results:
self._automation_result.blobs.append(upload_result.blob_id)
self._automation_result.blobs.extend(
[upload_result.blob_id for upload_result in upload_response.upload_results]
)

def mark_run_failed(self, status_message: str) -> None:
"""Mark the current run a failure."""
Expand All @@ -279,28 +306,101 @@ def _mark_run(
self._automation_result.run_status = status
self._automation_result.elapsed = duration

msg = f"Automation run {status.value} after {duration:2f} seconds."
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)

def add_object_error(self, object_id: str, error_cause: str) -> None:
"""Add an error to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.ERROR, error_cause)
def attach_error_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new error case to the run results.
def add_object_warning(self, object_id: str, warning: str) -> None:
"""Add a warning to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.WARNING, warning)
If the error cause has already created an error case,
the error will be extended with a new case refering to the causing objects.
Args:
error_tag (str): A short tag for the error type.
causing_object_ids (str[]): A list of object_id-s that are causing the error
error_messagge (Optional[str]): Optional error message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
self.attach_result_to_objects(
ObjectResultLevel.ERROR,
category,
object_ids,
message,
metadata,
visual_overrides,
)

def attach_warning_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new warning case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.WARNING,
category,
object_ids,
message,
metadata,
visual_overrides,
)

def add_object_info(self, object_id: str, info: str) -> None:
"""Add an info message to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.INFO, info)
def attach_info_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new info case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.INFO,
category,
object_ids,
message,
metadata,
visual_overrides,
)

def _add_object_result(
self, object_id: str, level: ObjectResultLevel, status_message: str
def attach_result_to_objects(
self,
level: ObjectResultLevel,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
if isinstance(object_ids, list):
if len(object_ids) < 1:
raise ValueError(
f"Need atleast one object_id to report a(n) {level.value.upper()}"
)
id_list = object_ids
else:
id_list = [object_ids]
print(
f"Object {object_id} was marked with {level.value.upper()}",
f" cause: {status_message}",
f"Object {', '.join(id_list)} was marked with {level.value.upper()}",
f"/{category} cause: {message}",
)
self._automation_result.object_results[object_id].append(
ObjectResult(level=level, status_message=status_message)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
54 changes: 54 additions & 0 deletions src/speckle_automate/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Some useful helpers for working with automation data."""
import secrets
import string

from specklepy.api.client import SpeckleClient
from gql import gql


def register_new_automation(
speckle_client: SpeckleClient,
project_id: str,
model_id: str,
automation_id: str,
automation_name: str,
automation_revision_id: str,
) -> bool:
"""Register a new automation in the speckle server."""
query = gql(
"""
mutation CreateAutomation(
$projectId: String!
$modelId: String!
$automationName: String!
$automationId: String!
$automationRevisionId: String!
) {
automationMutations {
create(
input: {
projectId: $projectId
modelId: $modelId
automationName: $automationName
automationId: $automationId
automationRevisionId: $automationRevisionId
}
)
}
}
"""
)
params = {
"projectId": project_id,
"modelId": model_id,
"automationName": automation_name,
"automationId": automation_id,
"automationRevisionId": automation_revision_id,
}
return speckle_client.httpclient.execute(query, params)


def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
21 changes: 8 additions & 13 deletions src/speckle_automate/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def execute_automate_function(
raise ValueError("Cannot get speckle token from arguments or environment")

speckle_token = speckle_token if speckle_token else args[3]
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)

inputs = (
input_schema.model_validate_json(function_inputs)
Expand All @@ -75,16 +78,14 @@ def execute_automate_function(

if inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
automation_run_data,
speckle_token,
inputs,
)
else:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
automation_run_data,
speckle_token,
)

exit_code = (
Expand All @@ -98,33 +99,27 @@ def execute_automate_function(

@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
inputs: T,
) -> AutomationContext:
...


@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
) -> AutomationContext:
...


def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context.report_run_status()

try:
Expand Down
Loading

0 comments on commit 52c8e37

Please sign in to comment.