Compare commits

...

22 Commits

Author SHA1 Message Date
Jonathon Broughton abc62f0811 AutomateGenerateJsonSchema(GenerateJsonSchema)
Adds additional processing the the OOTB GenerateJsonSchema for types created in pydantic that moves the requirement from users function to the SDK
2024-08-18 16:28:22 +01:00
Gergő Jedlicska fe03d96ae2 Merge pull request #346 from specklesystems/charles/trailingSlash
fix(automate): remove extra slash
2024-08-11 13:31:17 +02:00
Charles Driesler 078a6c8da8 fix(automate): extra slash 2024-08-10 23:17:45 +01:00
Iain Sproat 905377dea1 feat(default domain): app.speckle.systems is now default over speckle.xyz (#343)
- also updates the example email domain to use the IANA owned example domain instead of a production or random domain
2024-07-18 17:19:32 +02:00
Gergő Jedlicska 62c5114cb3 Merge pull request #341 from specklesystems/gergo/fixtures_no_init
fix: remove fixtures from automate exports
2024-06-07 18:53:20 +02:00
Gergő Jedlicska 43a5302a90 fix: tures 2024-06-07 18:51:03 +02:00
Gergő Jedlicska addaa996ea fix: remove fixtures from automate exports 2024-06-07 18:42:19 +02:00
Gergő Jedlicska 3b5421a5bc Merge pull request #340 from specklesystems/gergo/automateExceptionOutcome
feat: add excetion outcome reporting to functions
2024-06-07 15:29:40 +02:00
Gergő Jedlicska 88e8c86fa6 feat: add excetion outcome reporting to functions 2024-06-07 11:13:15 +02:00
Chuck Driesler d6843b9971 Merge pull request #339 from specklesystems/chuck/testAutomationHelpers
WEB-1053 Create helpers for testing automate functions
2024-06-06 12:03:00 +01:00
Charles Driesler 302a9f7f30 repair import 2024-06-06 12:01:00 +01:00
Charles Driesler ede9591c6a export fixtures 2024-06-06 11:58:18 +01:00
Charles Driesler c5b339d891 deps deps deps 2024-06-05 16:59:05 +01:00
Charles Driesler 2e35fb9e5c create helpers for testing functions 2024-06-05 16:38:49 +01:00
Gergő Jedlicska e6b822b0e3 Merge pull request #338 from specklesystems/gergo/automateExitCode
fix(automate): make sure we exit with code 0 if execution completes
2024-06-03 16:08:28 +02:00
Gergő Jedlicska 239bc4b5b9 docs(automate): finish comment block thoughts 2024-06-03 14:29:29 +02:00
Gergő Jedlicska 4eea15ddc1 fix(automate): make sure we exit with code 0 if execution completes 2024-06-03 14:27:07 +02:00
Aleksei Protopopov 204aa7466e Feature: adds connection_timeout argument to SpeckleClient (#337)
* Add connection_timeout argument to SpeckleClient

* Reformat code with black

* Set default timeout to 10s

* Make connection retries configurable
2024-05-27 14:23:39 +01:00
Gergő Jedlicska 24019e99f3 Merge pull request #335 from specklesystems/gergo/automateInterfaceRework
Rework automate SDK for the integrated automate api
2024-05-16 18:14:47 +02:00
Gergő Jedlicska 64492fafa5 fix: proper pytest skip 2024-05-16 17:24:53 +02:00
Gergő Jedlicska 3a8d634989 test: disable automation tests for now 2024-05-16 17:18:57 +02:00
Gergő Jedlicska f27650af3a feat: update automation schema and automation context for the new automate interfaces 2024-05-16 10:25:58 +02:00
17 changed files with 1135 additions and 951 deletions
Generated
+761 -744
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -33,10 +33,11 @@ pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.0.187"
ruff = "^0.4.4"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
+1
View File
@@ -1,4 +1,5 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
+48 -62
View File
@@ -1,4 +1,5 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
@@ -17,6 +18,7 @@ from speckle_automate.schema import (
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
@@ -94,8 +96,10 @@ class AutomationContext:
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, self.automation_run_data.version_id
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
@@ -104,7 +108,7 @@ class AutomationContext:
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {self.automation_run_data.version_id}",
f" the speckle version {version_id}",
)
return base
@@ -119,19 +123,27 @@ class AutomationContext:
version_message (str): The message for the new version.
"""
if model_name == self.automation_run_data.branch_name:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {self.automation_run_data.model_id} /"
f" {self.automation_run_data.branch_name}"
)
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
# we just check if it exists
if (not branch) or isinstance(branch, SpeckleException):
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
@@ -139,8 +151,6 @@ class AutomationContext:
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
else:
model_id = branch.id
root_object_id = operations.send(
root_object,
@@ -174,7 +184,8 @@ class AutomationContext:
) -> None:
link_resources = (
[
f"{self.automation_run_data.model_id}@{self.automation_run_data.version_id}"
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
@@ -194,47 +205,26 @@ class AutomationContext:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation ReportFunctionRunStatus(
$automationId: String!,
$automationRevisionId: String!,
$automationRunId: String!,
$versionId: String!,
$functionId: String!,
$functionName: String!,
$functionLogo: String,
$runStatus: AutomationRunStatus!
$elapsed: Float!
$contextView: String
$resultVersionIds: [String!]!
mutation AutomateFunctionRunStatusReport(
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$objectResults: JSONObject
$results: JSONObject
$contextView: String
){
automationMutations {
functionRunStatusReport(input: {
automationId: $automationId
automationRevisionId: $automationRevisionId
automationRunId: $automationRunId
versionId: $versionId
functionRuns: [
{
functionId: $functionId
functionName: $functionName
functionLogo: $functionLogo
status: $runStatus,
contextView: $contextView,
elapsed: $elapsed,
resultVersionIds: $resultVersionIds,
statusMessage: $statusMessage
results: $objectResults
}]
})
}
automateFunctionRunStatusReport(input: {
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": "1.0.0",
"version": 1,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
@@ -246,19 +236,11 @@ class AutomationContext:
object_results = None
params = {
"automationId": self.automation_run_data.automation_id,
"automationRevisionId": self.automation_run_data.automation_revision_id,
"automationRunId": self.automation_run_data.automation_run_id,
"versionId": self.automation_run_data.version_id,
"functionId": self.automation_run_data.function_id,
"functionName": self.automation_run_data.function_name,
"functionLogo": self.automation_run_data.function_logo,
"runStatus": self.run_status.value,
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
"results": object_results,
"contextView": self._automation_result.result_view,
"elapsed": self.elapsed(),
"resultVersionIds": self._automation_result.result_versions,
"objectResults": object_results,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
@@ -282,7 +264,7 @@ class AutomationContext:
files = {path_obj.name: open(str(path_obj), "rb")}
url = (
f"{self.automation_run_data.speckle_server_url}/api/stream/"
f"{self.automation_run_data.speckle_server_url}api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
@@ -308,6 +290,10 @@ class AutomationContext:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.FAILED, status_message)
def mark_run_exception(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.EXCEPTION, status_message)
def mark_run_success(self, status_message: Optional[str]) -> None:
"""Mark the current run a success with an optional message."""
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
+154
View File
@@ -0,0 +1,154 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
import pytest
from gql import gql
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from speckle_automate.schema import AutomationRunData, TestAutomationRunData
from specklepy.api.client import SpeckleClient
class TestAutomationEnvironment(BaseSettings):
"""Get known environment variables from local `.env` file"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="speckle_",
extra="ignore",
)
token: str = Field()
server_url: str = Field()
project_id: str = Field()
automation_id: str = Field()
@pytest.fixture()
def test_automation_environment() -> TestAutomationEnvironment:
return TestAutomationEnvironment()
@pytest.fixture()
def test_automation_token(
test_automation_environment: TestAutomationEnvironment,
) -> str:
"""Provide a speckle token for the test suite."""
return test_automation_environment.token
@pytest.fixture()
def speckle_client(
test_automation_environment: TestAutomationEnvironment,
) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
speckle_client = SpeckleClient(
test_automation_environment.server_url,
test_automation_environment.server_url.startswith("https"),
)
speckle_client.authenticate_with_token(test_automation_environment.token)
return speckle_client
def create_test_automation_run(
speckle_client: SpeckleClient, project_id: str, test_automation_id: str
) -> TestAutomationRunData:
"""Create test run to report local test results to"""
query = gql(
"""
mutation CreateTestRun(
$projectId: ID!,
$automationId: ID!
) {
projectMutations {
automationMutations(projectId: $projectId) {
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
triggers {
payload {
modelId
versionId
}
triggerType
}
}
}
}
}
"""
)
params = {"automationId": test_automation_id, "projectId": project_id}
result = speckle_client.httpclient.execute(query, params)
print(result)
return (
result.get("projectMutations")
.get("automationMutations")
.get("createTestAutomationRun")
)
@pytest.fixture()
def test_automation_run(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> TestAutomationRunData:
return create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
def create_test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
"""Create automation run data for a new run for a given test automation"""
test_automation_run_data = create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
return AutomationRunData(
project_id=test_automation_environment.project_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data["automationRunId"],
function_run_id=test_automation_run_data["functionRunId"],
triggers=test_automation_run_data["triggers"],
)
@pytest.fixture()
def test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
return create_test_automation_run_data(speckle_client, test_automation_environment)
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()
__all__ = [
"test_automation_environment",
"test_automation_token",
"speckle_client",
"test_automation_run",
"test_automation_run_data",
]
-55
View File
@@ -1,55 +0,0 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
from gql import gql
from specklepy.api.client import SpeckleClient
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()
+78 -11
View File
@@ -3,11 +3,13 @@
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
from enum import Enum
from pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
@@ -65,16 +67,11 @@ def execute_automate_function(
@overload
def execute_automate_function(automate_function: AutomateFunctionWithoutInputs) -> None:
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
def generate(self, schema, mode="validation"):
json_schema = super().generate(schema, mode=mode)
json_schema["$schema"] = self.schema_dialect
return json_schema
def execute_automate_function(
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
@@ -127,8 +124,10 @@ def execute_automate_function(
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit_code = (
0 if automation_context.run_status == AutomationStatus.SUCCEEDED else 1
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
@@ -173,6 +172,7 @@ def run_function(
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
@@ -181,7 +181,7 @@ def run_function(
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_failed(
automation_context.mark_run_exception(
"Function error. Check the automation run logs for details."
)
finally:
@@ -189,3 +189,70 @@ def run_function(
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
class AutomateGenerateJsonSchema(GenerateJsonSchema):
def __init__(self, by_alias: bool = True, ref_template: str = "#/$defs/{model}"):
super().__init__(by_alias=by_alias, ref_template=ref_template)
self.schema_dialect = "https://json-schema.org/draft/2020-12/schema"
def generate(self, schema, mode="validation"):
json_schema = super().generate(schema, mode=mode)
json_schema["$schema"] = self.schema_dialect
if "properties" in json_schema:
for prop, details in json_schema["properties"].items():
self._process_property(
details, json_schema.get("$defs", {}), getattr(schema, prop, None)
)
if "$defs" in json_schema:
for def_name, def_schema in json_schema["$defs"].items():
self._process_property(def_schema, json_schema["$defs"], None)
return json_schema
def _process_property(self, property_schema, defs, field):
if "allOf" in property_schema and len(property_schema["allOf"]) == 1:
ref = property_schema["allOf"][0].get("$ref")
if ref and ref.startswith("#/$defs/"):
enum_name = ref.split("/")[-1]
if enum_name in defs:
enum_schema = defs[enum_name]
property_schema.update(enum_schema)
del property_schema["allOf"]
if "enum" in property_schema:
enum_values = property_schema["enum"]
property_schema["oneOf"] = [
{"const": value, "title": str(value).upper()} for value in enum_values
]
del property_schema["enum"]
if isinstance(field, Enum):
property_schema["oneOf"] = [
{"const": item.value, "title": item.name} for item in field.__class__
]
if "default" in property_schema:
property_schema["default"] = property_schema["default"].value
if "type" not in property_schema:
if "oneOf" in property_schema:
property_schema["type"] = "string"
elif "default" in property_schema:
property_schema["type"] = self._infer_type(property_schema["default"])
else:
property_schema["type"] = "object"
@staticmethod
def _infer_type(value):
if isinstance(value, bool):
return "boolean"
elif isinstance(value, int):
return "integer"
elif isinstance(value, float):
return "number"
elif isinstance(value, str):
return "string"
else:
return "object"
+32 -9
View File
@@ -1,6 +1,7 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
@@ -12,22 +13,43 @@ class AutomateBase(BaseModel):
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
"""Represents the version creation trigger payload."""
model_id: str
version_id: str
class VersionCreationTrigger(AutomateBase):
"""Represents a single version creation trigger for the automation run."""
trigger_type: Literal["versionCreation"]
payload: VersionCreationTriggerPayload
class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
model_id: str
branch_name: str
version_id: str
speckle_server_url: str
automation_id: str
automation_revision_id: str
automation_run_id: str
function_run_id: str
function_id: str
function_name: str
function_logo: Optional[str]
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class TestAutomationRunData(BaseModel):
"""Values of the run created in the test automation for local test results."""
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
@@ -41,6 +63,7 @@ class AutomationStatus(str, Enum):
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
+3 -3
View File
@@ -21,7 +21,7 @@ class SpeckleClient(CoreSpeckleClient):
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -32,7 +32,7 @@ class SpeckleClient(CoreSpeckleClient):
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -47,7 +47,7 @@ class SpeckleClient(CoreSpeckleClient):
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
USE_SSL = True
def __init__(
+5 -2
View File
@@ -1,8 +1,9 @@
from typing import Optional
from typing import Optional, Union
from specklepy.api.models import Branch
from specklepy.core.api.resources.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -31,7 +32,9 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return super().create(stream_id, name, description)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
def get(
self, stream_id: str, name: str, commits_limit: int = 10
) -> Union[Branch, None, SpeckleException]:
"""Get a branch by name from a stream
Arguments:
+1 -1
View File
@@ -22,7 +22,7 @@ class StreamWrapper(CoreStreamWrapper):
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
+11 -4
View File
@@ -30,7 +30,7 @@ class SpeckleClient:
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -41,7 +41,7 @@ class SpeckleClient:
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -56,7 +56,7 @@ class SpeckleClient:
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
USE_SSL = True
def __init__(
@@ -64,6 +64,8 @@ class SpeckleClient:
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
ws_protocol = "ws"
http_protocol = "http"
@@ -80,10 +82,15 @@ class SpeckleClient:
self.ws_url = f"{ws_protocol}://{host}/graphql"
self.account = Account()
self.verify_certificate = verify_certificate
self.connection_retries = connection_retries
self.connection_timeout = connection_timeout
self.httpclient = Client(
transport=RequestsHTTPTransport(
url=self.graphql, verify=self.verify_certificate, retries=3
url=self.graphql,
verify=self.verify_certificate,
retries=self.connection_retries,
timeout=self.connection_timeout,
)
)
self.wsclient = None
+1 -1
View File
@@ -30,7 +30,7 @@ class StreamWrapper:
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
+1 -1
View File
@@ -23,7 +23,7 @@ def host():
def seed_user(host):
seed = uuid.uuid4().hex
user_dict = {
"email": f"{seed[0:7]}@spockle.com",
"email": f"{seed[0:7]}@example.org",
"password": "$uper$3cr3tP@ss",
"name": f"{seed[0:7]} Name",
"company": "test spockle",
@@ -12,12 +12,13 @@ from speckle_automate import (
AutomationStatus,
run_function,
)
from speckle_automate.helpers import crypto_random_string, register_new_automation
from speckle_automate.fixtures import (
create_test_automation_run_data,
crypto_random_string,
)
from speckle_automate.schema import AutomateBase
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
@pytest.fixture
@@ -40,58 +41,16 @@ def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
return test_client
@pytest.fixture
def test_object() -> Base:
"""Create a Base model for testing."""
root_object = Base()
root_object.foo = "bar"
return root_object
@pytest.fixture
def automation_run_data(
test_object: Base, test_client: SpeckleClient, speckle_server_url: str
test_client: SpeckleClient, speckle_server_url: str
) -> AutomationRunData:
"""Set up an automation context for testing."""
project_id = test_client.stream.create("Automate function e2e test")
branch_name = "main"
"""TODO: Set up a test automation for integration testing"""
project_id = crypto_random_string(10)
test_automation_id = crypto_random_string(10)
model = test_client.branch.get(project_id, branch_name, commits_limit=1)
model_id: str = model.id
root_obj_id = operations.send(
test_object, [ServerTransport(project_id, test_client)]
)
version_id = test_client.commit.create(project_id, root_obj_id)
automation_name = crypto_random_string(10)
automation_id = crypto_random_string(10)
automation_revision_id = crypto_random_string(10)
register_new_automation(
test_client,
project_id,
model_id,
automation_id,
automation_name,
automation_revision_id,
)
automation_run_id = crypto_random_string(10)
function_id = crypto_random_string(10)
function_name = f"automate test {crypto_random_string(3)}"
return AutomationRunData(
project_id=project_id,
model_id=model_id,
branch_name=branch_name,
version_id=version_id,
speckle_server_url=speckle_server_url,
automation_id=automation_id,
automation_revision_id=automation_revision_id,
automation_run_id=automation_run_id,
function_id=function_id,
function_name=function_name,
function_logo=None,
return create_test_automation_run_data(
test_client, speckle_server_url, project_id, test_automation_id
)
@@ -189,6 +148,9 @@ def automate_function(
automation_context.mark_run_success("No forbidden types found.")
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_function_run(automation_context: AutomationContext) -> None:
"""Run an integration test for the automate function."""
automation_context = run_function(
@@ -215,6 +177,9 @@ def test_file_path():
os.remove(path)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_file_uploads(
automation_run_data: AutomationRunData, speckle_token: str, test_file_path: Path
):
@@ -230,6 +195,9 @@ def test_file_uploads(
assert len(automation_context._automation_result.blobs) == 1
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project_raises_error_for_same_model(
automation_context: AutomationContext,
) -> None:
@@ -239,6 +207,9 @@ def test_create_version_in_project_raises_error_for_same_model(
)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project(
automation_context: AutomationContext,
) -> None:
@@ -252,6 +223,9 @@ def test_create_version_in_project(
assert version_id is not None
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_set_context_view(automation_context: AutomationContext) -> None:
automation_context.set_context_view()
+2 -2
View File
@@ -183,7 +183,7 @@ class TestStream:
# NOTE: only works for server admins
# invited = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
# emails=["userA@example.org", "userB@example.org"],
# user_ids=[second_user.id],
# message="yeehaw 🤠",
# )
@@ -192,7 +192,7 @@ class TestStream:
# invited_only_email = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userC@speckle.xyz"],
# emails=["userC@example.org"],
# message="yeehaw 🤠",
# )
+10 -4
View File
@@ -100,16 +100,20 @@ def test_parse_globals_as_commit():
#! NOTE: the following three tests may not pass locally
# if you have a `speckle.xyz` account in manager
# if you have a `app.speckle.systems` account in manager
def test_get_client_without_auth():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert client is not None
def test_get_new_client_with_token(user_path):
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
client = wrap.get_client(token="super-secret-token")
@@ -117,7 +121,9 @@ def test_get_new_client_with_token(user_path):
def test_get_transport_with_token():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert not client.account.token # unauthenticated bc no local accounts