Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f7113f0c5 | |||
| 7f38b24421 | |||
| cac4551fdf | |||
| 813bc8be4d | |||
| a45ae03235 | |||
| 905377dea1 | |||
| 62c5114cb3 | |||
| 43a5302a90 | |||
| addaa996ea | |||
| 3b5421a5bc | |||
| 88e8c86fa6 | |||
| d6843b9971 | |||
| 302a9f7f30 | |||
| ede9591c6a | |||
| c5b339d891 | |||
| 2e35fb9e5c | |||
| e6b822b0e3 | |||
| 239bc4b5b9 | |||
| 4eea15ddc1 | |||
| 204aa7466e | |||
| 24019e99f3 | |||
| 64492fafa5 | |||
| 3a8d634989 | |||
| f27650af3a | |||
| 6469b6f757 | |||
| b28db0881c | |||
| b0b442de23 | |||
| 32d2fe8ead | |||
| 9fd40eac23 | |||
| b22ba1f1f1 | |||
| 5e20fe7bf1 | |||
| 6da5da23c4 |
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
|
|||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
|
|
||||||
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
|
||||||
ENV PATH=$PATH:$HOME/.poetry/env
|
ENV PATH=$PATH:$HOME/.poetry/env
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at our public server
|
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||||
|
|
||||||
### Resources
|
### Resources
|
||||||
|
|||||||
Generated
+761
-744
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -33,10 +33,11 @@ pytest-ordering = "^0.6"
|
|||||||
pytest-cov = "^3.0.0"
|
pytest-cov = "^3.0.0"
|
||||||
devtools = "^0.8.0"
|
devtools = "^0.8.0"
|
||||||
pylint = "^2.14.4"
|
pylint = "^2.14.4"
|
||||||
|
pydantic-settings = "^2.3.0"
|
||||||
mypy = "^0.982"
|
mypy = "^0.982"
|
||||||
pre-commit = "^2.20.0"
|
pre-commit = "^2.20.0"
|
||||||
commitizen = "^2.38.0"
|
commitizen = "^2.38.0"
|
||||||
ruff = "^0.0.187"
|
ruff = "^0.4.4"
|
||||||
types-deprecated = "^1.2.9"
|
types-deprecated = "^1.2.9"
|
||||||
types-ujson = "^5.6.0.0"
|
types-ujson = "^5.6.0.0"
|
||||||
types-requests = "^2.28.11.5"
|
types-requests = "^2.28.11.5"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""This module contains an SDK for working with Speckle Automate."""
|
"""This module contains an SDK for working with Speckle Automate."""
|
||||||
|
|
||||||
from speckle_automate.automation_context import AutomationContext
|
from speckle_automate.automation_context import AutomationContext
|
||||||
from speckle_automate.runner import execute_automate_function, run_function
|
from speckle_automate.runner import execute_automate_function, run_function
|
||||||
from speckle_automate.schema import (
|
from speckle_automate.schema import (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""This module provides an abstraction layer above the Speckle Automate runtime."""
|
"""This module provides an abstraction layer above the Speckle Automate runtime."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -17,6 +18,7 @@ from speckle_automate.schema import (
|
|||||||
)
|
)
|
||||||
from specklepy.api import operations
|
from specklepy.api import operations
|
||||||
from specklepy.api.client import SpeckleClient
|
from specklepy.api.client import SpeckleClient
|
||||||
|
from specklepy.core.api.models import Branch
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
from specklepy.transports.memory import MemoryTransport
|
from specklepy.transports.memory import MemoryTransport
|
||||||
@@ -94,8 +96,10 @@ class AutomationContext:
|
|||||||
|
|
||||||
def receive_version(self) -> Base:
|
def receive_version(self) -> Base:
|
||||||
"""Receive the Speckle project version that triggered this automation run."""
|
"""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(
|
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:
|
if not commit.referencedObject:
|
||||||
raise ValueError("The commit has no referencedObject, cannot receive it.")
|
raise ValueError("The commit has no referencedObject, cannot receive it.")
|
||||||
@@ -104,7 +108,7 @@ class AutomationContext:
|
|||||||
)
|
)
|
||||||
print(
|
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}",
|
f" the speckle version {version_id}",
|
||||||
)
|
)
|
||||||
return base
|
return base
|
||||||
|
|
||||||
@@ -119,19 +123,27 @@ class AutomationContext:
|
|||||||
version_message (str): The message for the new version.
|
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(
|
branch = self.speckle_client.branch.get(
|
||||||
self.automation_run_data.project_id, model_name, 1
|
self.automation_run_data.project_id, model_name, 1
|
||||||
)
|
)
|
||||||
# we just check if it exists
|
if isinstance(branch, Branch):
|
||||||
if (not branch) or isinstance(branch, SpeckleException):
|
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(
|
branch_create = self.speckle_client.branch.create(
|
||||||
self.automation_run_data.project_id,
|
self.automation_run_data.project_id,
|
||||||
model_name,
|
model_name,
|
||||||
@@ -139,8 +151,6 @@ class AutomationContext:
|
|||||||
if isinstance(branch_create, Exception):
|
if isinstance(branch_create, Exception):
|
||||||
raise branch_create
|
raise branch_create
|
||||||
model_id = branch_create
|
model_id = branch_create
|
||||||
else:
|
|
||||||
model_id = branch.id
|
|
||||||
|
|
||||||
root_object_id = operations.send(
|
root_object_id = operations.send(
|
||||||
root_object,
|
root_object,
|
||||||
@@ -174,7 +184,8 @@ class AutomationContext:
|
|||||||
) -> None:
|
) -> None:
|
||||||
link_resources = (
|
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
|
if include_source_model_version
|
||||||
else []
|
else []
|
||||||
@@ -194,47 +205,26 @@ class AutomationContext:
|
|||||||
"""Report the current run status to the project of this automation."""
|
"""Report the current run status to the project of this automation."""
|
||||||
query = gql(
|
query = gql(
|
||||||
"""
|
"""
|
||||||
mutation ReportFunctionRunStatus(
|
mutation AutomateFunctionRunStatusReport(
|
||||||
$automationId: String!,
|
$functionRunId: String!
|
||||||
$automationRevisionId: String!,
|
$status: AutomateRunStatus!
|
||||||
$automationRunId: String!,
|
|
||||||
$versionId: String!,
|
|
||||||
$functionId: String!,
|
|
||||||
$functionName: String!,
|
|
||||||
$functionLogo: String,
|
|
||||||
$runStatus: AutomationRunStatus!
|
|
||||||
$elapsed: Float!
|
|
||||||
$contextView: String
|
|
||||||
$resultVersionIds: [String!]!
|
|
||||||
$statusMessage: String
|
$statusMessage: String
|
||||||
$objectResults: JSONObject
|
$results: JSONObject
|
||||||
|
$contextView: String
|
||||||
){
|
){
|
||||||
automationMutations {
|
automateFunctionRunStatusReport(input: {
|
||||||
functionRunStatusReport(input: {
|
functionRunId: $functionRunId
|
||||||
automationId: $automationId
|
status: $status
|
||||||
automationRevisionId: $automationRevisionId
|
statusMessage: $statusMessage
|
||||||
automationRunId: $automationRunId
|
contextView: $contextView
|
||||||
versionId: $versionId
|
results: $results
|
||||||
functionRuns: [
|
})
|
||||||
{
|
|
||||||
functionId: $functionId
|
|
||||||
functionName: $functionName
|
|
||||||
functionLogo: $functionLogo
|
|
||||||
status: $runStatus,
|
|
||||||
contextView: $contextView,
|
|
||||||
elapsed: $elapsed,
|
|
||||||
resultVersionIds: $resultVersionIds,
|
|
||||||
statusMessage: $statusMessage
|
|
||||||
results: $objectResults
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
|
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
|
||||||
object_results = {
|
object_results = {
|
||||||
"version": "1.0.0",
|
"version": 1,
|
||||||
"values": {
|
"values": {
|
||||||
"objectResults": self._automation_result.model_dump(by_alias=True)[
|
"objectResults": self._automation_result.model_dump(by_alias=True)[
|
||||||
"objectResults"
|
"objectResults"
|
||||||
@@ -246,19 +236,11 @@ class AutomationContext:
|
|||||||
object_results = None
|
object_results = None
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"automationId": self.automation_run_data.automation_id,
|
"functionRunId": self.automation_run_data.function_run_id,
|
||||||
"automationRevisionId": self.automation_run_data.automation_revision_id,
|
"status": self.run_status.value,
|
||||||
"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,
|
|
||||||
"statusMessage": self._automation_result.status_message,
|
"statusMessage": self._automation_result.status_message,
|
||||||
|
"results": object_results,
|
||||||
"contextView": self._automation_result.result_view,
|
"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}")
|
print(f"Reporting run status with content: {params}")
|
||||||
self.speckle_client.httpclient.execute(query, params)
|
self.speckle_client.httpclient.execute(query, params)
|
||||||
@@ -308,6 +290,10 @@ class AutomationContext:
|
|||||||
"""Mark the current run a failure."""
|
"""Mark the current run a failure."""
|
||||||
self._mark_run(AutomationStatus.FAILED, status_message)
|
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:
|
def mark_run_success(self, status_message: Optional[str]) -> None:
|
||||||
"""Mark the current run a success with an optional message."""
|
"""Mark the current run a success with an optional message."""
|
||||||
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
|
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
@@ -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()
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
Provides mechanisms to execute any function,
|
Provides mechanisms to execute any function,
|
||||||
that conforms to the AutomateFunction "interface"
|
that conforms to the AutomateFunction "interface"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@@ -65,7 +66,9 @@ def execute_automate_function(
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def execute_automate_function(automate_function: AutomateFunctionWithoutInputs) -> None:
|
def execute_automate_function(
|
||||||
|
automate_function: AutomateFunctionWithoutInputs,
|
||||||
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@@ -127,8 +130,10 @@ def execute_automate_function(
|
|||||||
automate_function, # type: ignore
|
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 = (
|
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)
|
exit(exit_code)
|
||||||
|
|
||||||
@@ -173,6 +178,7 @@ def run_function(
|
|||||||
if automation_context.run_status not in [
|
if automation_context.run_status not in [
|
||||||
AutomationStatus.FAILED,
|
AutomationStatus.FAILED,
|
||||||
AutomationStatus.SUCCEEDED,
|
AutomationStatus.SUCCEEDED,
|
||||||
|
AutomationStatus.EXCEPTION,
|
||||||
]:
|
]:
|
||||||
automation_context.mark_run_success(
|
automation_context.mark_run_success(
|
||||||
"WARNING: Automate assumed a success status,"
|
"WARNING: Automate assumed a success status,"
|
||||||
@@ -181,7 +187,7 @@ def run_function(
|
|||||||
except Exception:
|
except Exception:
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
print(trace)
|
print(trace)
|
||||||
automation_context.mark_run_failed(
|
automation_context.mark_run_exception(
|
||||||
"Function error. Check the automation run logs for details."
|
"Function error. Check the automation run logs for details."
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
""""""
|
""""""
|
||||||
|
|
||||||
from enum import Enum
|
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 pydantic import BaseModel, ConfigDict, Field
|
||||||
from stringcase import camelcase
|
from stringcase import camelcase
|
||||||
@@ -12,22 +13,43 @@ class AutomateBase(BaseModel):
|
|||||||
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
|
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):
|
class AutomationRunData(BaseModel):
|
||||||
"""Values of the project / model that triggered the run of this function."""
|
"""Values of the project / model that triggered the run of this function."""
|
||||||
|
|
||||||
project_id: str
|
project_id: str
|
||||||
model_id: str
|
|
||||||
branch_name: str
|
|
||||||
version_id: str
|
|
||||||
speckle_server_url: str
|
speckle_server_url: str
|
||||||
|
|
||||||
automation_id: str
|
automation_id: str
|
||||||
automation_revision_id: str
|
|
||||||
automation_run_id: str
|
automation_run_id: str
|
||||||
|
function_run_id: str
|
||||||
|
|
||||||
function_id: str
|
triggers: List[VersionCreationTrigger]
|
||||||
function_name: str
|
|
||||||
function_logo: Optional[str]
|
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(
|
model_config = ConfigDict(
|
||||||
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
|
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
|
||||||
@@ -41,6 +63,7 @@ class AutomationStatus(str, Enum):
|
|||||||
RUNNING = "RUNNING"
|
RUNNING = "RUNNING"
|
||||||
FAILED = "FAILED"
|
FAILED = "FAILED"
|
||||||
SUCCEEDED = "SUCCEEDED"
|
SUCCEEDED = "SUCCEEDED"
|
||||||
|
EXCEPTION = "EXCEPTION"
|
||||||
|
|
||||||
|
|
||||||
class ObjectResultLevel(str, Enum):
|
class ObjectResultLevel(str, Enum):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class SpeckleClient(CoreSpeckleClient):
|
|||||||
The `SpeckleClient` is your entry point for interacting with
|
The `SpeckleClient` is your entry point for interacting with
|
||||||
your Speckle Server's GraphQL API.
|
your Speckle Server's GraphQL API.
|
||||||
You'll need to have access to a server to use it,
|
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
|
To authenticate the client, you'll need to have downloaded
|
||||||
the [Speckle Manager](https://speckle.guide/#speckle-manager)
|
the [Speckle Manager](https://speckle.guide/#speckle-manager)
|
||||||
@@ -32,7 +32,7 @@ class SpeckleClient(CoreSpeckleClient):
|
|||||||
from specklepy.api.credentials import get_default_account
|
from specklepy.api.credentials import get_default_account
|
||||||
|
|
||||||
# initialise the client
|
# 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
|
# 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)
|
# 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
|
USE_SSL = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from specklepy.logging import metrics
|
|||||||
|
|
||||||
|
|
||||||
class Resource(CoreResource):
|
class Resource(CoreResource):
|
||||||
"""API Access class for users"""
|
"""API Access class for users. This class provides methods to get and update
|
||||||
|
the user profile, fetch user activity, and manage pending stream invitations."""
|
||||||
|
|
||||||
def __init__(self, account, basepath, client, server_version) -> None:
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -19,13 +20,9 @@ class Resource(CoreResource):
|
|||||||
self.schema = User
|
self.schema = User
|
||||||
|
|
||||||
def get(self) -> User:
|
def get(self) -> User:
|
||||||
"""Gets the profile of a user. If no id argument is provided,
|
"""Gets the profile of the current authenticated user's profile
|
||||||
will return the current authenticated user's profile
|
|
||||||
(as extracted from the authorization header).
|
(as extracted from the authorization header).
|
||||||
|
|
||||||
Arguments:
|
|
||||||
id {str} -- the user id
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
User -- the retrieved user
|
User -- the retrieved user
|
||||||
"""
|
"""
|
||||||
@@ -41,11 +38,11 @@ class Resource(CoreResource):
|
|||||||
):
|
):
|
||||||
"""Updates your user profile. All arguments are optional.
|
"""Updates your user profile. All arguments are optional.
|
||||||
|
|
||||||
Arguments:
|
Args:
|
||||||
name {str} -- your name
|
name (Optional[str]): The user's name.
|
||||||
company {str} -- the company you may or may not work for
|
company (Optional[str]): The company the user works for.
|
||||||
bio {str} -- tell us about yourself
|
bio (Optional[str]): A brief user biography.
|
||||||
avatar {str} -- a nice photo of yourself
|
avatar (Optional[str]): A URL to an avatar image for the user.
|
||||||
|
|
||||||
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
|
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
|
||||||
bool -- True if your profile was updated successfully
|
bool -- True if your profile was updated successfully
|
||||||
@@ -62,35 +59,30 @@ class Resource(CoreResource):
|
|||||||
cursor: Optional[datetime] = None,
|
cursor: Optional[datetime] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Get the activity from a given stream in an Activity collection.
|
Fetches collection the current authenticated user's activity
|
||||||
Step into the activity `items` for the list of activity.
|
as filtered by given parameters
|
||||||
If no id argument is provided, will return the current authenticated user's
|
|
||||||
activity (as extracted from the authorization header).
|
|
||||||
|
|
||||||
Note: all timestamps arguments should be `datetime` of any tz as they will be
|
Note: all timestamps arguments should be `datetime` of any tz as they will be
|
||||||
converted to UTC ISO format strings
|
converted to UTC ISO format strings
|
||||||
|
|
||||||
user_id {str} -- the id of the user to get the activity from
|
Args:
|
||||||
action_type {str} -- filter results to a single action type
|
limit (int): The maximum number of activity items to return.
|
||||||
(eg: `commit_create` or `commit_receive`)
|
action_type (Optional[str]): Filter results to a single action type.
|
||||||
limit {int} -- max number of Activity items to return
|
before (Optional[datetime]): Latest cutoff for activity to include.
|
||||||
before {datetime} -- latest cutoff for activity
|
after (Optional[datetime]): Oldest cutoff for an activity to include.
|
||||||
(ie: return all activity _before_ this time)
|
cursor (Optional[datetime]): Timestamp cursor for pagination.
|
||||||
after {datetime} -- oldest cutoff for activity
|
|
||||||
(ie: return all activity _after_ this time)
|
Returns:
|
||||||
cursor {datetime} -- timestamp cursor for pagination
|
Activity collection, filtered according to the provided parameters.
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
|
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
|
||||||
return super().activity(limit, action_type, before, after, cursor)
|
return super().activity(limit, action_type, before, after, cursor)
|
||||||
|
|
||||||
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
|
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
|
||||||
"""Get all of the active user's pending stream invites
|
"""Fetches all of the current user's pending stream invitations.
|
||||||
|
|
||||||
Requires Speckle Server version >= 2.6.4
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[PendingStreamCollaborator]
|
List[PendingStreamCollaborator]: A list of pending stream invitations.
|
||||||
-- a list of pending invites for the current user
|
|
||||||
"""
|
"""
|
||||||
metrics.track(
|
metrics.track(
|
||||||
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
|
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
|
||||||
@@ -100,18 +92,14 @@ class Resource(CoreResource):
|
|||||||
def get_pending_invite(
|
def get_pending_invite(
|
||||||
self, stream_id: str, token: Optional[str] = None
|
self, stream_id: str, token: Optional[str] = None
|
||||||
) -> Optional[PendingStreamCollaborator]:
|
) -> Optional[PendingStreamCollaborator]:
|
||||||
"""Get a particular pending invite for the active user on a given stream.
|
"""Fetches a specific pending invite for the current user on a given stream.
|
||||||
If no invite_id is provided, any valid invite will be returned.
|
|
||||||
|
|
||||||
Requires Speckle Server version >= 2.6.4
|
Args:
|
||||||
|
stream_id (str): The ID of the stream to look for invites on.
|
||||||
Arguments:
|
token (Optional[str]): The token of the invite to look for (optional).
|
||||||
stream_id {str} -- the id of the stream to look for invites on
|
|
||||||
token {str} -- the token of the invite to look for (optional)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PendingStreamCollaborator
|
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
|
||||||
-- the invite for the given stream (or None if it isn't found)
|
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
|
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
|
||||||
return super().get_pending_invite(stream_id, token)
|
return super().get_pending_invite(stream_id, token)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
from specklepy.api.models import Branch
|
from specklepy.api.models import Branch
|
||||||
from specklepy.core.api.resources.branch import Resource as CoreResource
|
from specklepy.core.api.resources.branch import Resource as CoreResource
|
||||||
from specklepy.logging import metrics
|
from specklepy.logging import metrics
|
||||||
|
from specklepy.logging.exceptions import SpeckleException
|
||||||
|
|
||||||
|
|
||||||
class Resource(CoreResource):
|
class Resource(CoreResource):
|
||||||
@@ -31,7 +32,9 @@ class Resource(CoreResource):
|
|||||||
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
|
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
|
||||||
return super().create(stream_id, name, description)
|
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
|
"""Get a branch by name from a stream
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ from specklepy.logging.exceptions import SpeckleException
|
|||||||
|
|
||||||
|
|
||||||
class Resource(CoreResource):
|
class Resource(CoreResource):
|
||||||
"""API Access class for other users, that are not the currently active user."""
|
"""
|
||||||
|
Provides API access to other users' profiles and activities on the platform.
|
||||||
|
This class enables fetching limited information about users, searching for users by name or email,
|
||||||
|
and accessing user activity logs with appropriate privacy and access control measures in place.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, account, basepath, client, server_version) -> None:
|
def __init__(self, account, basepath, client, server_version) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -21,13 +25,13 @@ class Resource(CoreResource):
|
|||||||
|
|
||||||
def get(self, id: str) -> LimitedUser:
|
def get(self, id: str) -> LimitedUser:
|
||||||
"""
|
"""
|
||||||
Gets the profile of another user.
|
Retrieves the profile of a user specified by their user ID.
|
||||||
|
|
||||||
Arguments:
|
Args:
|
||||||
id {str} -- the user id
|
id (str): The unique identifier of the user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
LimitedUser -- the retrieved profile of another user
|
LimitedUser: The profile of the user with limited information.
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
|
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
|
||||||
return super().get(id)
|
return super().get(id)
|
||||||
@@ -35,18 +39,21 @@ class Resource(CoreResource):
|
|||||||
def search(
|
def search(
|
||||||
self, search_query: str, limit: int = 25
|
self, search_query: str, limit: int = 25
|
||||||
) -> Union[List[LimitedUser], SpeckleException]:
|
) -> Union[List[LimitedUser], SpeckleException]:
|
||||||
"""Searches for user by name or email. The search query must be at least
|
"""
|
||||||
3 characters long
|
Searches for users by name or email.
|
||||||
|
The search requires a minimum query length of 3 characters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_query (str): The search string.
|
||||||
|
limit (int): Maximum number of search results to return.
|
||||||
|
|
||||||
Arguments:
|
|
||||||
search_query {str} -- a string to search for
|
|
||||||
limit {int} -- the maximum number of results to return
|
|
||||||
Returns:
|
Returns:
|
||||||
List[LimitedUser] -- a list of User objects that match the search query
|
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
|
||||||
|
query or an exception if the query is too short.
|
||||||
"""
|
"""
|
||||||
if len(search_query) < 3:
|
if len(search_query) < 3:
|
||||||
return SpeckleException(
|
return SpeckleException(
|
||||||
message="User search query must be at least 3 characters"
|
message="User search query must be at least 3 characters."
|
||||||
)
|
)
|
||||||
|
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
|
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
|
||||||
@@ -62,21 +69,19 @@ class Resource(CoreResource):
|
|||||||
cursor: Optional[datetime] = None,
|
cursor: Optional[datetime] = None,
|
||||||
) -> ActivityCollection:
|
) -> ActivityCollection:
|
||||||
"""
|
"""
|
||||||
Get the activity from a given stream in an Activity collection.
|
Retrieves a collection of activities for a specified user, with optional filters for activity type,
|
||||||
Step into the activity `items` for the list of activity.
|
time frame, and pagination.
|
||||||
|
|
||||||
Note: all timestamps arguments should be `datetime` of
|
Args:
|
||||||
any tz as they will be converted to UTC ISO format strings
|
user_id (str): The ID of the user whose activities are being requested.
|
||||||
|
limit (int): The maximum number of activity items to return.
|
||||||
|
action_type (Optional[str]): A specific type of activity to filter.
|
||||||
|
before (Optional[datetime]): Latest timestamp to include activities before.
|
||||||
|
after (Optional[datetime]): Earliest timestamp to include activities after.
|
||||||
|
cursor (Optional[datetime]): Timestamp for pagination cursor.
|
||||||
|
|
||||||
user_id {str} -- the id of the user to get the activity from
|
Returns:
|
||||||
action_type {str} -- filter results to a single action type
|
ActivityCollection: A collection of user activities filtered according to specified criteria.
|
||||||
(eg: `commit_create` or `commit_receive`)
|
|
||||||
limit {int} -- max number of Activity items to return
|
|
||||||
before {datetime} -- latest cutoff for activity
|
|
||||||
(ie: return all activity _before_ this time)
|
|
||||||
after {datetime} -- oldest cutoff for activity
|
|
||||||
(ie: return all activity _after_ this time)
|
|
||||||
cursor {datetime} -- timestamp cursor for pagination
|
|
||||||
"""
|
"""
|
||||||
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
|
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
|
||||||
return super().activity(user_id, limit, action_type, before, after, cursor)
|
return super().activity(user_id, limit, action_type, before, after, cursor)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class StreamWrapper(CoreStreamWrapper):
|
|||||||
from specklepy.api.wrapper import StreamWrapper
|
from specklepy.api.wrapper import StreamWrapper
|
||||||
|
|
||||||
# provide any stream, branch, commit, object, or globals url
|
# 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
|
# get an authenticated SpeckleClient if you have a local account for the server
|
||||||
client = wrapper.get_client()
|
client = wrapper.get_client()
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class SpeckleClient:
|
|||||||
The `SpeckleClient` is your entry point for interacting with
|
The `SpeckleClient` is your entry point for interacting with
|
||||||
your Speckle Server's GraphQL API.
|
your Speckle Server's GraphQL API.
|
||||||
You'll need to have access to a server to use it,
|
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
|
To authenticate the client, you'll need to have downloaded
|
||||||
the [Speckle Manager](https://speckle.guide/#speckle-manager)
|
the [Speckle Manager](https://speckle.guide/#speckle-manager)
|
||||||
@@ -41,7 +41,7 @@ class SpeckleClient:
|
|||||||
from specklepy.api.credentials import get_default_account
|
from specklepy.api.credentials import get_default_account
|
||||||
|
|
||||||
# initialise the client
|
# 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
|
# 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)
|
# 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
|
USE_SSL = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -64,6 +64,8 @@ class SpeckleClient:
|
|||||||
host: str = DEFAULT_HOST,
|
host: str = DEFAULT_HOST,
|
||||||
use_ssl: bool = USE_SSL,
|
use_ssl: bool = USE_SSL,
|
||||||
verify_certificate: bool = True,
|
verify_certificate: bool = True,
|
||||||
|
connection_retries: int = 3,
|
||||||
|
connection_timeout: int = 10,
|
||||||
) -> None:
|
) -> None:
|
||||||
ws_protocol = "ws"
|
ws_protocol = "ws"
|
||||||
http_protocol = "http"
|
http_protocol = "http"
|
||||||
@@ -80,10 +82,15 @@ class SpeckleClient:
|
|||||||
self.ws_url = f"{ws_protocol}://{host}/graphql"
|
self.ws_url = f"{ws_protocol}://{host}/graphql"
|
||||||
self.account = Account()
|
self.account = Account()
|
||||||
self.verify_certificate = verify_certificate
|
self.verify_certificate = verify_certificate
|
||||||
|
self.connection_retries = connection_retries
|
||||||
|
self.connection_timeout = connection_timeout
|
||||||
|
|
||||||
self.httpclient = Client(
|
self.httpclient = Client(
|
||||||
transport=RequestsHTTPTransport(
|
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
|
self.wsclient = None
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
|
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
|
|||||||
res = account_storage.get_all_objects()
|
res = account_storage.get_all_objects()
|
||||||
account_storage.close()
|
account_storage.close()
|
||||||
if res:
|
if res:
|
||||||
accounts.extend(Account.model_validate_json(r[1]) for r in res)
|
accounts.extend(Account.parse_raw(r[1]) for r in res)
|
||||||
except SpeckleException:
|
except SpeckleException:
|
||||||
# cannot open SQLiteTransport, probably because of the lack
|
# cannot open SQLiteTransport, probably because of the lack
|
||||||
# of disk write permissions
|
# of disk write permissions
|
||||||
@@ -78,7 +79,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
|
|||||||
if json_acct_files:
|
if json_acct_files:
|
||||||
try:
|
try:
|
||||||
accounts.extend(
|
accounts.extend(
|
||||||
Account.model_validate_json(Path(json_path, json_file).read_text())
|
Account.parse_raw(Path(json_path, json_file).read_text())
|
||||||
# Account.parse_file(os.path.join(json_path, json_file))
|
# Account.parse_file(os.path.join(json_path, json_file))
|
||||||
for json_file in json_acct_files
|
for json_file in json_acct_files
|
||||||
)
|
)
|
||||||
@@ -142,6 +143,28 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
|
|||||||
return Account.from_token(token, server_url)
|
return Account.from_token(token, server_url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_accounts_for_server(host: str) -> List[Account]:
|
||||||
|
all_accounts = get_local_accounts()
|
||||||
|
filtered: List[Account] = []
|
||||||
|
|
||||||
|
for acc in all_accounts:
|
||||||
|
moved_from = (
|
||||||
|
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if moved_from and host == urlparse(moved_from).netloc:
|
||||||
|
filtered.append(acc)
|
||||||
|
|
||||||
|
for acc in all_accounts:
|
||||||
|
if any([x for x in filtered if x.userInfo.id == acc.userInfo.id]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if host == urlparse(acc.serverInfo.url).netloc:
|
||||||
|
filtered.append(acc)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
class StreamWrapper:
|
class StreamWrapper:
|
||||||
def __init__(self, url: str = None) -> None:
|
def __init__(self, url: str = None) -> None:
|
||||||
raise SpeckleException(
|
raise SpeckleException(
|
||||||
|
|||||||
@@ -185,6 +185,11 @@ class ActivityCollection(BaseModel):
|
|||||||
return self.__repr__()
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
|
class ServerMigration(BaseModel):
|
||||||
|
movedTo: Optional[str] = None
|
||||||
|
movedFrom: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ServerInfo(BaseModel):
|
class ServerInfo(BaseModel):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
company: Optional[str] = None
|
company: Optional[str] = None
|
||||||
@@ -197,3 +202,4 @@ class ServerInfo(BaseModel):
|
|||||||
authStrategies: Optional[List[dict]] = None
|
authStrategies: Optional[List[dict]] = None
|
||||||
version: Optional[str] = None
|
version: Optional[str] = None
|
||||||
frontend2: Optional[bool] = None
|
frontend2: Optional[bool] = None
|
||||||
|
migration: Optional[ServerMigration] = None
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from specklepy.core.api.client import SpeckleClient
|
|||||||
from specklepy.core.api.credentials import (
|
from specklepy.core.api.credentials import (
|
||||||
Account,
|
Account,
|
||||||
get_account_from_token,
|
get_account_from_token,
|
||||||
get_local_accounts,
|
get_accounts_for_server,
|
||||||
)
|
)
|
||||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||||
from specklepy.transports.server.server import ServerTransport
|
from specklepy.transports.server.server import ServerTransport
|
||||||
@@ -30,7 +30,7 @@ class StreamWrapper:
|
|||||||
from specklepy.api.wrapper import StreamWrapper
|
from specklepy.api.wrapper import StreamWrapper
|
||||||
|
|
||||||
# provide any stream, branch, commit, object, or globals url
|
# 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
|
# get an authenticated SpeckleClient if you have a local account for the server
|
||||||
client = wrapper.get_client()
|
client = wrapper.get_client()
|
||||||
@@ -178,14 +178,7 @@ class StreamWrapper:
|
|||||||
if self._account and self._account.token:
|
if self._account and self._account.token:
|
||||||
return self._account
|
return self._account
|
||||||
|
|
||||||
self._account = next(
|
self._account = next(iter(get_accounts_for_server(self.host)), None)
|
||||||
(
|
|
||||||
a
|
|
||||||
for a in get_local_accounts()
|
|
||||||
if self.host == urlparse(a.serverInfo.url).netloc
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self._account:
|
if not self._account:
|
||||||
self._account = get_account_from_token(token, self.server_url)
|
self._account = get_account_from_token(token, self.server_url)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from specklepy.objects.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class GisFeature(
|
||||||
|
Base, speckle_type="Objects.GIS.GisFeature", detachable={"displayValue"}
|
||||||
|
):
|
||||||
|
"""GIS Feature"""
|
||||||
|
|
||||||
|
geometry: Optional[List[Base]] = None
|
||||||
|
attributes: Base
|
||||||
|
displayValue: Optional[List[Base]] = None
|
||||||
@@ -23,7 +23,7 @@ def host():
|
|||||||
def seed_user(host):
|
def seed_user(host):
|
||||||
seed = uuid.uuid4().hex
|
seed = uuid.uuid4().hex
|
||||||
user_dict = {
|
user_dict = {
|
||||||
"email": f"{seed[0:7]}@spockle.com",
|
"email": f"{seed[0:7]}@example.org",
|
||||||
"password": "$uper$3cr3tP@ss",
|
"password": "$uper$3cr3tP@ss",
|
||||||
"name": f"{seed[0:7]} Name",
|
"name": f"{seed[0:7]} Name",
|
||||||
"company": "test spockle",
|
"company": "test spockle",
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ from speckle_automate import (
|
|||||||
AutomationStatus,
|
AutomationStatus,
|
||||||
run_function,
|
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 speckle_automate.schema import AutomateBase
|
||||||
from specklepy.api import operations
|
|
||||||
from specklepy.api.client import SpeckleClient
|
from specklepy.api.client import SpeckleClient
|
||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
from specklepy.transports.server import ServerTransport
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -40,58 +41,16 @@ def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
|||||||
return test_client
|
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
|
@pytest.fixture
|
||||||
def automation_run_data(
|
def automation_run_data(
|
||||||
test_object: Base, test_client: SpeckleClient, speckle_server_url: str
|
test_client: SpeckleClient, speckle_server_url: str
|
||||||
) -> AutomationRunData:
|
) -> AutomationRunData:
|
||||||
"""Set up an automation context for testing."""
|
"""TODO: Set up a test automation for integration testing"""
|
||||||
project_id = test_client.stream.create("Automate function e2e test")
|
project_id = crypto_random_string(10)
|
||||||
branch_name = "main"
|
test_automation_id = crypto_random_string(10)
|
||||||
|
|
||||||
model = test_client.branch.get(project_id, branch_name, commits_limit=1)
|
return create_test_automation_run_data(
|
||||||
model_id: str = model.id
|
test_client, speckle_server_url, project_id, test_automation_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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -189,6 +148,9 @@ def automate_function(
|
|||||||
automation_context.mark_run_success("No forbidden types found.")
|
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:
|
def test_function_run(automation_context: AutomationContext) -> None:
|
||||||
"""Run an integration test for the automate function."""
|
"""Run an integration test for the automate function."""
|
||||||
automation_context = run_function(
|
automation_context = run_function(
|
||||||
@@ -215,6 +177,9 @@ def test_file_path():
|
|||||||
os.remove(path)
|
os.remove(path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skip(
|
||||||
|
"currently the function run cannot be integration tested with the server"
|
||||||
|
)
|
||||||
def test_file_uploads(
|
def test_file_uploads(
|
||||||
automation_run_data: AutomationRunData, speckle_token: str, test_file_path: Path
|
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
|
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(
|
def test_create_version_in_project_raises_error_for_same_model(
|
||||||
automation_context: AutomationContext,
|
automation_context: AutomationContext,
|
||||||
) -> None:
|
) -> 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(
|
def test_create_version_in_project(
|
||||||
automation_context: AutomationContext,
|
automation_context: AutomationContext,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -252,6 +223,9 @@ def test_create_version_in_project(
|
|||||||
assert version_id is not None
|
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:
|
def test_set_context_view(automation_context: AutomationContext) -> None:
|
||||||
automation_context.set_context_view()
|
automation_context.set_context_view()
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class TestStream:
|
|||||||
# NOTE: only works for server admins
|
# NOTE: only works for server admins
|
||||||
# invited = client.stream.invite_batch(
|
# invited = client.stream.invite_batch(
|
||||||
# stream_id=stream.id,
|
# stream_id=stream.id,
|
||||||
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
|
# emails=["userA@example.org", "userB@example.org"],
|
||||||
# user_ids=[second_user.id],
|
# user_ids=[second_user.id],
|
||||||
# message="yeehaw 🤠",
|
# message="yeehaw 🤠",
|
||||||
# )
|
# )
|
||||||
@@ -192,7 +192,7 @@ class TestStream:
|
|||||||
|
|
||||||
# invited_only_email = client.stream.invite_batch(
|
# invited_only_email = client.stream.invite_batch(
|
||||||
# stream_id=stream.id,
|
# stream_id=stream.id,
|
||||||
# emails=["userC@speckle.xyz"],
|
# emails=["userC@example.org"],
|
||||||
# message="yeehaw 🤠",
|
# message="yeehaw 🤠",
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|||||||
@@ -100,16 +100,20 @@ def test_parse_globals_as_commit():
|
|||||||
|
|
||||||
|
|
||||||
#! NOTE: the following three tests may not pass locally
|
#! 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():
|
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()
|
client = wrap.get_client()
|
||||||
|
|
||||||
assert client is not None
|
assert client is not None
|
||||||
|
|
||||||
|
|
||||||
def test_get_new_client_with_token(user_path):
|
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()
|
||||||
client = wrap.get_client(token="super-secret-token")
|
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():
|
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()
|
client = wrap.get_client()
|
||||||
assert not client.account.token # unauthenticated bc no local accounts
|
assert not client.account.token # unauthenticated bc no local accounts
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from specklepy.core.api.credentials import Account, UserInfo, get_accounts_for_server
|
||||||
|
from specklepy.core.api.models import ServerInfo, ServerMigration
|
||||||
|
from specklepy.core.helpers import speckle_path_provider
|
||||||
|
|
||||||
|
|
||||||
|
def _create_account(
|
||||||
|
id: str, url: str, movedFrom: Optional[str], movedTo: Optional[str]
|
||||||
|
) -> Account:
|
||||||
|
return Account(
|
||||||
|
id=uuid.uuid4().hex[:6].lower(),
|
||||||
|
token="myToken",
|
||||||
|
serverInfo=ServerInfo(
|
||||||
|
url=url,
|
||||||
|
name="myServer",
|
||||||
|
migration=ServerMigration(movedTo=movedTo, movedFrom=movedFrom),
|
||||||
|
),
|
||||||
|
userInfo=UserInfo(id=id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _test_cases() -> List[Tuple[List[Account], str, List[Account]]]:
|
||||||
|
user_id_1 = uuid.uuid4().hex[:6].lower()
|
||||||
|
user_id_2 = uuid.uuid4().hex[:6].lower()
|
||||||
|
old = _create_account(
|
||||||
|
user_id_1, "https://old.example.com", None, "https://new.example.com"
|
||||||
|
)
|
||||||
|
new = _create_account(
|
||||||
|
user_id_1, "https://new.example.com", "https://old.example.com", None
|
||||||
|
)
|
||||||
|
other = _create_account(user_id_2, "https://other.example.com", None, None)
|
||||||
|
|
||||||
|
given_accounts = [old, new, other]
|
||||||
|
reversed = [other, new, old]
|
||||||
|
|
||||||
|
return [
|
||||||
|
(given_accounts, "https://old.example.com", [new]),
|
||||||
|
(given_accounts, "https://new.example.com", [new]),
|
||||||
|
(reversed, "https://old.example.com", [new]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_accounts(accounts: List[Account]) -> None:
|
||||||
|
json_accounts = speckle_path_provider.accounts_folder_path()
|
||||||
|
|
||||||
|
for acc in accounts:
|
||||||
|
# deleting acc json file in json_accounts path
|
||||||
|
os.remove(os.path.join(json_accounts, f"{acc.id}.json"))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _add_accounts(accounts: List[Account]) -> None:
|
||||||
|
json_accounts = speckle_path_provider.accounts_folder_path()
|
||||||
|
|
||||||
|
for acc in accounts:
|
||||||
|
data = Account.model_dump_json(acc)
|
||||||
|
with open(os.path.join(json_accounts, f"{acc.id}.json"), "w") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("accounts, requested_url, expected", _test_cases())
|
||||||
|
def test_server_migration(
|
||||||
|
accounts: List[Account], requested_url: str, expected: List[Account]
|
||||||
|
) -> None:
|
||||||
|
_add_accounts(accounts)
|
||||||
|
try:
|
||||||
|
res = get_accounts_for_server(urlparse(requested_url).netloc)
|
||||||
|
assert res == expected
|
||||||
|
|
||||||
|
finally:
|
||||||
|
_clean_accounts(accounts)
|
||||||
Reference in New Issue
Block a user