Compare commits

..

9 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
11 changed files with 103 additions and 28 deletions
+1 -2
View File
@@ -1,5 +1,5 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate import fixtures
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
@@ -21,5 +21,4 @@ __all__ = [
"ObjectResultLevel",
"run_function",
"execute_automate_function",
"fixtures",
]
+5 -1
View File
@@ -264,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 = (
@@ -290,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)
+75 -10
View File
@@ -9,6 +9,7 @@ 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
@@ -70,14 +71,7 @@ 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],
@@ -132,7 +126,10 @@ def execute_automate_function(
# 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(0)
exit_code = (
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
else:
raise NotImplementedError(f"Command: '{command}' is not supported.")
@@ -175,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,"
@@ -183,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:
@@ -191,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"
+1
View File
@@ -63,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__(
+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()
+3 -3
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__(
+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",
+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