Merge branch 'main' into kate-2.17-hotfix

This commit is contained in:
KatKatKateryna
2024-02-12 13:52:34 +00:00
14 changed files with 726 additions and 496 deletions
+5 -5
View File
@@ -6,11 +6,11 @@ repos:
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: 3.12.0
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v3.13.0
- repo: https://github.com/pycqa/isort
rev: 5.12.0
Generated
+460 -421
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.17.8"
version = "2.17.14"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -16,9 +16,9 @@ packages = [
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.0"
pydantic = "^2.5"
appdirs = "^1.4.4"
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
gql = { extras = ["requests", "websockets"], version = "^3.3.0" }
ujson = "^5.3.0"
Deprecated = "^1.2.13"
stringcase = "^1.2.0"
@@ -26,7 +26,7 @@ attrs = "^23.1.0"
httpx = "^0.25.0"
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
black = "23.11.0"
isort = "^5.7.0"
pytest = "^7.1.3"
pytest-ordering = "^0.6"
+65 -26
View File
@@ -4,14 +4,16 @@ Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import os
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, TypeVar, Union, overload
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
from pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
from speckle_automate.automation_context import AutomationContext
from speckle_automate.schema import AutomateBase, AutomationStatus
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
T = TypeVar("T", bound=AutomateBase)
@@ -19,6 +21,41 @@ AutomateFunction = Callable[[AutomationContext, T], None]
AutomateFunctionWithoutInputs = Callable[[AutomationContext], None]
def _read_input_data(inputs_location: str) -> str:
input_path = Path(inputs_location)
if not input_path.exists():
raise ValueError(f"Cannot find the function inputs file at {input_path}")
return input_path.read_text()
def _parse_input_data(
input_location: str, input_schema: Optional[type[T]]
) -> Tuple[AutomationRunData, Optional[T], str]:
input_json_string = _read_input_data(input_location)
class FunctionRunData(AutomateBase):
speckle_token: str
automation_run_data: AutomationRunData
function_inputs: None = None
parser_model = FunctionRunData
if input_schema:
parser_model = create_model(
"FunctionRunDataWithInputs",
function_inputs=(input_schema, ...),
__base__=FunctionRunData,
)
input_data = parser_model.model_validate_json(input_json_string)
return (
input_data.automation_run_data,
input_data.function_inputs,
input_data.speckle_token,
)
@overload
def execute_automate_function(
automate_function: AutomateFunction[T],
@@ -32,6 +69,13 @@ def execute_automate_function(automate_function: AutomateFunctionWithoutInputs)
...
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],
input_schema: Optional[type[T]] = None,
@@ -40,49 +84,44 @@ def execute_automate_function(
# first arg is the python file name, we do not need that
args = sys.argv[1:]
if len(args) < 2:
raise ValueError("too few arguments specified need minimum 2")
if len(args) > 4:
raise ValueError("too many arguments specified, max supported is 4")
if len(args) != 2:
raise ValueError("Incorrect number of arguments specified need 2")
# we rely on a command name convention to decide what to do.
# this is here, so that the function authors do not see any of this
command = args[0]
command, argument = args
if command == "generate_schema":
path = Path(args[1])
path = Path(argument)
schema = json.dumps(
input_schema.model_json_schema(by_alias=True) if input_schema else {}
input_schema.model_json_schema(
by_alias=True, schema_generator=AutomateGenerateJsonSchema
)
if input_schema
else {}
)
path.write_text(schema)
elif command == "run":
automation_run_data = args[1]
function_inputs = args[2]
automation_run_data, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
speckle_token = os.environ.get("SPECKLE_TOKEN", None)
if not speckle_token and len(args) != 4:
raise ValueError("Cannot get speckle token from arguments or environment")
speckle_token = speckle_token if speckle_token else args[3]
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
inputs = (
input_schema.model_validate_json(function_inputs)
if input_schema
else input_schema
)
if inputs:
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
inputs,
function_inputs, # type: ignore
)
else:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context = run_function(
automation_context,
automate_function, # type: ignore
+7 -1
View File
@@ -50,10 +50,16 @@ class SpeckleClient(CoreSpeckleClient):
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
) -> None:
super().__init__(
host=host,
use_ssl=use_ssl,
verify_certificate=verify_certificate,
)
self.account = Account()
+1 -2
View File
@@ -58,13 +58,12 @@ class SpeckleClient:
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
VERIFY_CERTIFICATE = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = VERIFY_CERTIFICATE,
verify_certificate: bool = True,
) -> None:
ws_protocol = "ws"
http_protocol = "http"
+1
View File
@@ -196,3 +196,4 @@ class ServerInfo(BaseModel):
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
@@ -4,6 +4,7 @@ from gql import gql
from specklepy.core.api.models import Branch
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch"
@@ -39,6 +40,10 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3:
return SpeckleException(
message="Branch Name must be at least 3 characters"
)
params = {
"branch": {
"streamId": stream_id,
+14 -1
View File
@@ -1,6 +1,7 @@
import re
from typing import Any, Dict, List, Tuple
import requests
from gql import gql
from specklepy.core.api.models import ServerInfo
@@ -56,9 +57,21 @@ class Resource(ResourceBase):
"""
)
return self.make_request(
server_info = self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
else:
server_info.frontend2 = False
return server_info
def version(self) -> Tuple[Any, ...]:
"""Get the server version
+4 -1
View File
@@ -166,7 +166,10 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3 and len(name) != 0:
return SpeckleException(
message="Stream Name must be at least 3 characters"
)
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
+91 -34
View File
@@ -1,4 +1,4 @@
from urllib.parse import unquote, urlparse
from urllib.parse import quote, unquote, urlparse
from warnings import warn
from gql import gql
@@ -47,6 +47,7 @@ class StreamWrapper:
commit_id: str = None
object_id: str = None
branch_name: str = None
model_id: str = None
_client: SpeckleClient = None
_account: Account = None
@@ -86,45 +87,58 @@ class StreamWrapper:
# check for fe2 URL
if "/projects/" in parsed.path:
use_fe2 = True
key_stream = "project"
else:
use_fe2 = False
key_stream = "stream"
while segments:
segment = segments.pop(0)
if segments and (
(use_fe2 is False and segment.lower() == "streams")
or (use_fe2 is True and segment.lower() == "projects")
):
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and (
(use_fe2 is False and segment.lower() == "branches")
or (use_fe2 is True and segment.lower() == "models")
):
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
if use_fe2 is False:
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
elif segments and use_fe2 is True:
if segment.lower() == "projects":
self.stream_id = segments.pop(0)
elif segment.lower() == "models":
next_segment = segments.pop(0)
if "," in next_segment:
raise SpeckleException("Multi-model urls are not supported yet")
elif unquote(next_segment).startswith("$"):
raise SpeckleException(
"Federation model urls are not supported"
)
elif len(next_segment) == 32:
self.object_id = next_segment
else:
self.branch_name = unquote(next_segment).split("@")[0]
if "@" in unquote(next_segment):
self.commit_id = unquote(next_segment).split("@")[1]
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
if use_fe2 is True and self.branch_name is not None:
if "," in self.branch_name:
raise SpeckleException("Multi-model urls are not supported yet")
if "@" in self.branch_name:
model_id = self.branch_name.split("@")[0]
self.commit_id = self.branch_name.split("@")[1]
else:
model_id = self.branch_name
self.model_id = self.branch_name
# get branch name
query = gql(
"""
@@ -139,7 +153,7 @@ class StreamWrapper:
"""
)
self._client = self.get_client()
params = {"project_id": self.stream_id, "model_id": model_id}
params = {"project_id": self.stream_id, "model_id": self.model_id}
project = self._client.httpclient.execute(query, params)
try:
@@ -149,7 +163,7 @@ class StreamWrapper:
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
)
@property
@@ -230,3 +244,46 @@ class StreamWrapper:
if not self._account or not self._account.token:
self.get_account(token)
return ServerTransport(self.stream_id, account=self._account)
def to_string(self) -> str:
"""
Constructs a URL depending on the StreamWrapper type and FE version.
"""
use_fe2 = False
key_streams = "/streams/"
key_branches = "/branches/"
if isinstance(self.branch_name, str):
value_branch = quote(self.branch_name)
if self.branch_name == "globals":
key_branches = "/"
key_commits = "/commits/"
if isinstance(self.commit_id, str) and self.branch_name == "globals":
key_commits = "/globals/"
key_objects = "/objects/"
if "/projects/" in self.stream_url:
use_fe2 = True
key_streams = "/projects/"
key_branches = "/models/"
value_branch = self.model_id
key_commits = "@"
key_objects = "/models/"
wrapper_type = self.type
if use_fe2 is False or (use_fe2 is True and not self.model_id):
base_url = f"{self.server_url}{key_streams}{self.stream_id}"
else: # fe2 is True and model_id available
base_url = f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
if wrapper_type == "object":
return f"{base_url}{key_objects}{self.object_id}"
elif wrapper_type == "commit":
return f"{base_url}{key_commits}{self.commit_id}"
elif wrapper_type == "branch":
return f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
elif wrapper_type == "stream":
return f"{self.server_url}{key_streams}{self.stream_id}"
else:
raise SpeckleException(
f"Cannot parse StreamWrapper of type '{wrapper_type}'"
)
+2 -1
View File
@@ -188,7 +188,8 @@ class _RegisteringBase:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
super().__init_subclass__(**kwargs)
# we know, that the super here is object, that takes no args on init subclass
return super().__init_subclass__()
# T = TypeVar("T")
+1
View File
@@ -18,6 +18,7 @@ class TestServer:
server = client.server.get()
assert isinstance(server, ServerInfo)
assert isinstance(server.frontend2, bool)
def test_server_version(self, client: SpeckleClient):
version = client.server.version()
+66
View File
@@ -2,11 +2,13 @@ import json
import tempfile
from pathlib import Path
from typing import Iterable
from urllib.parse import unquote
import pytest
from specklepy.api.wrapper import StreamWrapper
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
@pytest.fixture(scope="module", autouse=True)
@@ -29,6 +31,22 @@ def user_path() -> Iterable[Path]:
speckle_path_provider.override_application_data_path(None)
def test_parse_empty():
try:
StreamWrapper("https://testing.speckle.dev/streams")
assert False
except SpeckleException:
assert True
def test_parse_empty_fe2():
try:
StreamWrapper("https://latest.speckle.systems/projects")
assert False
except SpeckleException:
assert True
def test_parse_stream():
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
@@ -142,8 +160,56 @@ def test_parse_model():
assert wrap.type == "branch"
def test_parse_federated_model():
try:
StreamWrapper("https://latest.speckle.systems/projects/843d07eb10/models/$main")
assert False
except SpeckleException:
assert True
def test_parse_multi_model():
try:
StreamWrapper(
"https://latest.speckle.systems/projects/2099ac4b5f/models/1870f279e3,a9cfdddc79"
)
assert False
except SpeckleException:
assert True
def test_parse_object_fe2():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c"
)
assert wrap.type == "object"
def test_parse_version():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1"
)
wrap_quoted = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1"
)
assert wrap.type == "commit"
assert wrap_quoted.type == "commit"
def test_to_string():
urls = [
"https://testing.speckle.dev/streams/a75ab4f10f",
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F",
"https://testing.speckle.dev/streams/0c6ad366c4/globals",
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893",
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792",
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6",
"https://latest.speckle.systems/projects/843d07eb10",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1",
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c",
]
for url in urls:
wrap = StreamWrapper(url)
assert unquote(wrap.to_string()) == unquote(url)