Compare commits

..

1 Commits

Author SHA1 Message Date
Gergő Jedlicska 13e0b65c31 make test fail louder 2023-09-07 14:43:49 +02:00
79 changed files with 1470 additions and 3152 deletions
+3 -41
View File
@@ -1,47 +1,14 @@
version: 2.1
orbs:
codecov: codecov/codecov@3.3.0
python: circleci/python@2.0.3
codecov: codecov/codecov@3.2.2
jobs:
pre-commit:
parameters:
config_file:
default: ./.pre-commit-config.yaml
description: Optional, path to pre-commit config file.
type: string
cache_prefix:
default: ''
description: |
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
type: string
docker:
- image: speckle/pre-commit-runner:latest
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
- save_cache:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- run:
command: git --no-pager diff
name: git diff
when: on_fail
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: false
docker_layer_caching: true
resource_class: medium
parameters:
tag:
@@ -85,10 +52,6 @@ jobs:
workflows:
main:
jobs:
- pre-commit:
filters:
tags:
only: /.*/
- test:
matrix:
parameters:
@@ -99,7 +62,6 @@ workflows:
- deploy:
context: pypi
requires:
- pre-commit
- test
filters:
tags:
+1 -1
View File
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
USER vscode
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
ENV PATH=$PATH:$HOME/.poetry/env
+9 -9
View File
@@ -2,23 +2,23 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
- id: ruff
rev: v0.1.6
rev: v0.0.186
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v3.13.0
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v2.38.0
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: v5.11.3
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.11.0
rev: 22.12.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
@@ -27,7 +27,7 @@ repos:
# https://pre-commit.com/#top_level-default_language_version
# language_version: python3.11
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+1 -1
View File
@@ -33,7 +33,7 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
Give Speckle a try in no time by:
- [![speckle](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
+6 -6
View File
@@ -33,7 +33,7 @@ services:
retries: 30
minio:
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
image: "minio/minio"
command: server /data --console-address ":9001"
restart: always
volumes:
@@ -53,7 +53,7 @@ services:
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend:latest
image: speckle/speckle-frontend:2
restart: always
ports:
- "0.0.0.0:8080:8080"
@@ -61,7 +61,7 @@ services:
FILE_SIZE_LIMIT_MB: 100
speckle-server:
image: speckle/speckle-server:latest
image: speckle/speckle-server:2
restart: always
healthcheck:
test:
@@ -112,7 +112,7 @@ services:
ENABLE_MP: "false"
preview-service:
image: speckle/speckle-preview-service:latest
image: speckle/speckle-preview-service:2
restart: always
depends_on:
speckle-server:
@@ -124,7 +124,7 @@ services:
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
webhook-service:
image: speckle/speckle-webhook-service:latest
image: speckle/speckle-webhook-service:2
restart: always
depends_on:
speckle-server:
@@ -135,7 +135,7 @@ services:
WAIT_HOSTS: postgres:5432
fileimport-service:
image: speckle/speckle-fileimport-service:latest
image: speckle/speckle-fileimport-service:2
restart: always
depends_on:
speckle-server:
Generated
+955 -979
View File
File diff suppressed because it is too large Load Diff
+6 -9
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.17.14"
version = "2.9.1"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -10,34 +10,31 @@ documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
]
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.5"
python = ">=3.7.2, <4.0"
pydantic = "^2.0"
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"
attrs = "^23.1.0"
httpx = "^0.25.0"
[tool.poetry.group.dev.dependencies]
black = "23.11.0"
black = "^22.8.0"
isort = "^5.7.0"
pytest = "^7.1.3"
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.4.4"
ruff = "^0.0.187"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
-24
View File
@@ -1,24 +0,0 @@
"""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 (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
__all__ = [
"AutomationContext",
"AutomateBase",
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
]
-406
View File
@@ -1,406 +0,0 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
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
from specklepy.transports.server import ServerTransport
@dataclass
class AutomationContext:
"""A context helper class.
This class exposes methods to work with the Speckle Automate context inside
Speckle Automate functions.
An instance of AutomationContext is injected into every run of a function.
"""
automation_run_data: AutomationRunData
speckle_client: SpeckleClient
_server_transport: ServerTransport
_speckle_token: str
#: keep a memory transponrt at hand, to speed up things if needed
_memory_transport: MemoryTransport = field(default_factory=MemoryTransport)
#: added for performance measuring
_init_time: float = field(default_factory=time.perf_counter)
_automation_result: AutomationResult = field(default_factory=AutomationResult)
@classmethod
def initialize(
cls, automation_run_data: Union[str, AutomationRunData], speckle_token: str
) -> "AutomationContext":
"""Bootstrap the AutomateSDK from raw data.
Todo:
----
* bootstrap a structlog logger instance
* expose a logger, that ppl can use instead of print
"""
# parse the json value if its not an initialized project data instance
automation_run_data = (
automation_run_data
if isinstance(automation_run_data, AutomationRunData)
else AutomationRunData.model_validate_json(automation_run_data)
)
speckle_client = SpeckleClient(
automation_run_data.speckle_server_url,
automation_run_data.speckle_server_url.startswith("https"),
)
speckle_client.authenticate_with_token(speckle_token)
if not speckle_client.account:
msg = (
f"Could not autenticate to {automation_run_data.speckle_server_url}",
"with the provided token",
)
raise ValueError(msg)
server_transport = ServerTransport(
automation_run_data.project_id, speckle_client
)
return cls(automation_run_data, speckle_client, server_transport, speckle_token)
@property
def run_status(self) -> AutomationStatus:
"""Get the status of the automation run."""
return self._automation_result.run_status
@property
def status_message(self) -> Optional[str]:
"""Get the current status message."""
return self._automation_result.status_message
def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
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, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {version_id}",
)
return base
def create_new_version_in_project(
self, root_object: Base, model_name: str, version_message: str = ""
) -> Tuple[str, str]:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
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,
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
[self._server_transport, self._memory_transport],
use_default_cache=False,
)
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
branch_name=model_name,
message=version_message,
source_application="SpeckleAutomate",
)
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
@property
def context_view(self) -> Optional[str]:
return self._automation_result.result_view
def set_context_view(
self,
# f"{model_id}@{version_id} or {model_id} "
resource_ids: Optional[List[str]] = None,
include_source_model_version: bool = True,
) -> None:
link_resources = (
[
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
)
if resource_ids:
link_resources.extend(resource_ids)
if not link_resources:
raise Exception(
"We do not have enough resource ids to compose a context view"
)
self._automation_result.result_view = (
f"/projects/{self.automation_run_data.project_id}"
f"/models/{','.join(link_resources)}"
)
def report_run_status(self) -> None:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$results: JSONObject
$contextView: String
){
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,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None
params = {
"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,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
def store_file_result(self, file_path: Union[Path, str]) -> None:
"""Save a file attached to the project of this automation."""
path_obj = (
Path(file_path).resolve() if isinstance(file_path, str) else file_path
)
class UploadResult(AutomateBase):
blob_id: str
file_name: str
upload_status: int
class BlobUploadResponse(AutomateBase):
upload_results: list[UploadResult]
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
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.project_id}/blob"
)
data = (
httpx.post(
url,
files=files,
headers={"authorization": f"Bearer {self._speckle_token}"},
)
.raise_for_status()
.json()
)
upload_response = BlobUploadResponse.model_validate(data)
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")
self._automation_result.blobs.extend(
[upload_result.blob_id for upload_result in upload_response.upload_results]
)
def mark_run_failed(self, status_message: str) -> None:
"""Mark the current run a failure."""
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)
def _mark_run(
self, status: AutomationStatus, status_message: Optional[str]
) -> None:
duration = self.elapsed()
self._automation_result.status_message = status_message
self._automation_result.run_status = status
self._automation_result.elapsed = duration
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)
def attach_error_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new error case to the run results.
If the error cause has already created an error case,
the error will be extended with a new case refering to the causing objects.
Args:
error_tag (str): A short tag for the error type.
causing_object_ids (str[]): A list of object_id-s that are causing the error
error_messagge (Optional[str]): Optional error message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
self.attach_result_to_objects(
ObjectResultLevel.ERROR,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_warning_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new warning case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.WARNING,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_info_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new info case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.INFO,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_result_to_objects(
self,
level: ObjectResultLevel,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
if isinstance(object_ids, list):
if len(object_ids) < 1:
raise ValueError(
f"Need atleast one object_id to report a(n) {level.value.upper()}"
)
id_list = object_ids
else:
id_list = [object_ids]
print(
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
-154
View File
@@ -1,154 +0,0 @@
"""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",
]
-197
View File
@@ -1,197 +0,0 @@
"""Function execution module.
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 pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
from speckle_automate.automation_context import AutomationContext
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
T = TypeVar("T", bound=AutomateBase)
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],
input_schema: type[T],
) -> None:
...
@overload
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],
input_schema: Optional[type[T]] = None,
):
"""Runs the provided automate function with the input schema."""
# first arg is the python file name, we do not need that
args = sys.argv[1:]
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, argument = args
if command == "generate_schema":
path = Path(argument)
schema = json.dumps(
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, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
function_inputs, # type: ignore
)
else:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context = run_function(
automation_context,
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 = (
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
else:
raise NotImplementedError(f"Command: '{command}' is not supported.")
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext:
...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext:
...
def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context.report_run_status()
try:
# avoiding complex type gymnastics here on the internals.
# the external type overloads make this correct
if inputs:
automate_function(automation_context, inputs) # type: ignore
else:
automate_function(automation_context) # type: ignore
# the function author forgot to mark the function success
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
" but it was not marked as so by the function."
)
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_exception(
"Function error. Check the automation run logs for details."
)
finally:
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
-97
View File
@@ -1,97 +0,0 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
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
speckle_server_url: str
automation_id: str
automation_run_id: str
function_run_id: 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=()
)
class AutomationStatus(str, Enum):
"""Set the status of the automation."""
INITIALIZING = "INITIALIZING"
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class ResultCase(AutomateBase):
"""A result case."""
category: str
level: ObjectResultLevel
object_ids: List[str]
message: Optional[str]
metadata: Optional[Dict[str, Any]]
visual_overrides: Optional[Dict[str, Any]]
class AutomationResult(AutomateBase):
"""Schema accepted by the Speckle server as a result for an automation run."""
elapsed: float = 0
result_view: Optional[str] = None
result_versions: List[str] = Field(default_factory=list)
blobs: List[str] = Field(default_factory=list)
run_status: AutomationStatus = AutomationStatus.RUNNING
status_message: Optional[str] = None
object_results: list[ResultCase] = Field(default_factory=list)
-3
View File
@@ -1,3 +0,0 @@
from specklepy import objects
__all__ = ["objects"]
+19 -20
View File
@@ -1,7 +1,17 @@
from deprecated import deprecated
import re
from typing import Dict
from warnings import warn
from specklepy.api.credentials import Account
from deprecated import deprecated
from gql import Client
from gql.transport.exceptions import TransportServerError
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.api import resources
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.api.resources import (
user,
active_user,
branch,
commit,
@@ -10,10 +20,11 @@ from specklepy.api.resources import (
server,
stream,
subscriptions,
user,
)
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
class SpeckleClient(CoreSpeckleClient):
@@ -50,16 +61,10 @@ class SpeckleClient(CoreSpeckleClient):
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
) -> None:
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
super().__init__(
host=host,
use_ssl=use_ssl,
verify_certificate=verify_certificate,
)
self.account = Account()
@@ -126,9 +131,7 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"})
return super().authenticate(token)
def authenticate_with_token(self, token: str) -> None:
@@ -140,9 +143,7 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Token"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Token"})
return super().authenticate_with_token(token)
def authenticate_with_account(self, account: Account) -> None:
@@ -154,7 +155,5 @@ class SpeckleClient(CoreSpeckleClient):
account {Account} -- the account object which can be found with
`get_default_account` or `get_local_accounts`
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Account"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Account"})
return super().authenticate_with_account(account)
+17 -13
View File
@@ -1,14 +1,20 @@
import os
from typing import List, Optional
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import StreamWrapper # noqa: F401
from specklepy.core.api.credentials import Account, UserInfo # noqa: F401
from specklepy.core.api.credentials import (
get_account_from_token as core_get_account_from_token,
)
from specklepy.core.api.credentials import get_local_accounts as core_get_local_accounts
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.api.models import ServerInfo
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import (Account, UserInfo,
StreamWrapper, # deprecated
get_local_accounts as core_get_local_accounts,
get_account_from_token as core_get_account_from_token)
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
@@ -29,12 +35,11 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
{"name": "Get Local Accounts"},
{"name": "Get Local Accounts"}
)
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
@@ -56,8 +61,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
metrics.initialise_tracker(default)
return default
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
@@ -69,5 +73,5 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
"""
account = core_get_account_from_token(token, server_url)
metrics.track(metrics.SDK, account, {"name": "Get Account From Token"})
metrics.track( metrics.SDK, account, {"name": "Get Account From Token"} )
return account
+17 -73
View File
@@ -1,74 +1,18 @@
from specklepy.core.api.host_applications import (
ARCGIS,
ARCHICAD,
AUTOCAD,
BLENDER,
CIVIL,
CSIBRIDGE,
DXF,
DYNAMO,
ETABS,
EXCEL,
GRASSHOPPER,
GSA,
MICROSTATION,
NET,
OPENBUILDINGS,
OPENRAIL,
OPENROADS,
OTHER,
POWERBI,
PYTHON,
QGIS,
REVIT,
RHINO,
SAFE,
SAP2000,
SKETCHUP,
TEKLASTRUCTURES,
TOPSOLID,
UNITY,
UNREAL,
HostApplication,
HostAppVersion,
_app_name_host_app_mapping,
get_host_app_from_string,
)
from dataclasses import dataclass
from enum import Enum
from unicodedata import name
# re-exporting stuff from the moved api module
__all__ = [
"ARCGIS",
"ARCHICAD",
"AUTOCAD",
"BLENDER",
"CIVIL",
"CSIBRIDGE",
"DXF",
"DYNAMO",
"ETABS",
"EXCEL",
"GRASSHOPPER",
"GSA",
"MICROSTATION",
"NET",
"OPENBUILDINGS",
"OPENRAIL",
"OPENROADS",
"OTHER",
"POWERBI",
"PYTHON",
"QGIS",
"REVIT",
"RHINO",
"SAFE",
"SAP2000",
"SKETCHUP",
"TEKLASTRUCTURES",
"TOPSOLID",
"UNITY",
"UNREAL",
"HostApplication",
"HostAppVersion",
"_app_name_host_app_mapping",
"get_host_app_from_string",
]
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.host_applications import (HostApplication, HostAppVersion,
get_host_app_from_string,
_app_name_host_app_mapping,
RHINO,GRASSHOPPER,REVIT,DYNAMO,UNITY,GSA,
CIVIL,AUTOCAD,MICROSTATION,OPENROADS,
OPENRAIL,OPENBUILDINGS,ETABS,SAP2000,CSIBRIDGE,
SAFE,TEKLASTRUCTURES,DXF,EXCEL,UNREAL,POWERBI,
BLENDER,QGIS,ARCGIS,SKETCHUP,ARCHICAD,TOPSOLID,
PYTHON,NET,OTHER)
if __name__ == "__main__":
print(HostAppVersion.v)
+11 -34
View File
@@ -1,35 +1,12 @@
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
LimitedUser,
Object,
PendingStreamCollaborator,
ServerInfo,
Stream,
Streams,
User,
)
from datetime import datetime
from typing import List, Optional
__all__ = [
"Activity",
"ActivityCollection",
"Branch",
"Branches",
"Collaborator",
"Commit",
"Commits",
"LimitedUser",
"Object",
"PendingStreamCollaborator",
"ServerInfo",
"Stream",
"Streams",
"User",
]
from pydantic import BaseModel, Field
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (Collaborator, Commit,
Commits, Object, Branch, Branches,
Stream, Streams, User, LimitedUser,
PendingStreamCollaborator, Activity,
ActivityCollection, ServerInfo)
+8 -5
View File
@@ -1,12 +1,16 @@
from typing import List, Optional
from specklepy.core.api.operations import deserialize as core_deserialize
from specklepy.core.api.operations import receive as _untracked_receive
from specklepy.core.api.operations import send as core_send
from specklepy.core.api.operations import serialize as core_serialize
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.core.api.operations import (send as core_send,
receive as _untracked_receive,
serialize as core_serialize,
deserialize as core_deserialize)
def send(
@@ -70,7 +74,6 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
+20 -6
View File
@@ -1,8 +1,21 @@
from typing import Any, Optional, Tuple
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from specklepy.api.credentials import Account
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
UnsupportedException,
)
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.resource import ResourceBase as CoreResourceBase
@@ -16,9 +29,10 @@ class ResourceBase(CoreResourceBase):
server_version: Optional[Tuple[Any, ...]] = None,
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=name,
server_version=server_version,
account = account,
basepath = basepath,
client = client,
name = name,
server_version = server_version
)
+47 -32
View File
@@ -1,14 +1,18 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.active_user import Resource as CoreResource
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.active_user import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -20,9 +24,13 @@ class Resource(CoreResource):
self.schema = User
def get(self) -> User:
"""Gets the profile of the current authenticated user's profile
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
@@ -38,11 +46,11 @@ class Resource(CoreResource):
):
"""Updates your user profile. All arguments are optional.
Args:
name (Optional[str]): The user's name.
company (Optional[str]): The company the user works for.
bio (Optional[str]): A brief user biography.
avatar (Optional[str]): A URL to an avatar image for the user.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
@@ -59,47 +67,54 @@ class Resource(CoreResource):
cursor: Optional[datetime] = None,
):
"""
Fetches collection the current authenticated user's activity
as filtered by given parameters
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
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
converted to UTC ISO format strings
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(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": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
metrics.track(metrics.SDK, self.account, {"name": "User Active Invites All Get"})
return super().get_all_pending_invites()
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[str]): The token of the invite to look for (optional).
Requires Speckle Server version >= 2.6.4
Arguments:
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:
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
+7 -6
View File
@@ -1,9 +1,12 @@
from typing import Optional, Union
from typing import Optional
from gql import gql
from specklepy.api.models import Branch
from specklepy.core.api.resources.branch import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.branch import Resource as CoreResource
class Resource(CoreResource):
@@ -32,9 +35,7 @@ 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
) -> Union[Branch, None, SpeckleException]:
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
Arguments:
+9 -8
View File
@@ -1,9 +1,12 @@
from typing import List, Optional, Union
from typing import List, Optional
from gql import gql
from specklepy.api.models import Commit
from specklepy.core.api.resources.commit import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.commit import Resource as CoreResource
class Resource(CoreResource):
@@ -52,8 +55,8 @@ class Resource(CoreResource):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
@@ -73,9 +76,7 @@ class Resource(CoreResource):
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(
stream_id, object_id, branch_name, message, source_application, parents
)
return super().create(stream_id, object_id, branch_name, message, source_application, parents)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
+8 -2
View File
@@ -1,9 +1,14 @@
from typing import Dict, List
from specklepy.core.api.resources.object import Resource as CoreResource
from specklepy.logging import metrics
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
from specklepy.logging import metrics
from specklepy.core.api.resources.object import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for objects"""
@@ -53,3 +58,4 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
+34 -34
View File
@@ -1,18 +1,18 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.core.api.resources.other_user import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.other_user import Resource as CoreResource
class Resource(CoreResource):
"""
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.
"""
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -25,13 +25,13 @@ class Resource(CoreResource):
def get(self, id: str) -> LimitedUser:
"""
Retrieves the profile of a user specified by their user ID.
Gets the profile of another user.
Args:
id (str): The unique identifier of the user.
Arguments:
id {str} -- the user id
Returns:
LimitedUser: The profile of the user with limited information.
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
@@ -39,25 +39,22 @@ class Resource(CoreResource):
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""
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.
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
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"})
return super().search(search_query, limit)
return super().search(search_query, limit)
def activity(
self,
@@ -69,19 +66,22 @@ class Resource(CoreResource):
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Args:
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.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(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"})
return super().activity(user_id, limit, action_type, before, after, cursor)
return super().activity(user_id, limit, action_type, before, after, cursor)
+8 -2
View File
@@ -1,8 +1,14 @@
import re
from typing import Any, Dict, List, Tuple
from gql import gql
from specklepy.api.models import ServerInfo
from specklepy.core.api.resources.server import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
from specklepy.core.api.resources.server import Resource as CoreResource
class Resource(CoreResource):
@@ -67,4 +73,4 @@ class Resource(CoreResource):
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
return super().revoke_token(token)
return super().revoke_token(token)
+11 -8
View File
@@ -1,9 +1,15 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional
from specklepy.api.models import PendingStreamCollaborator, Stream
from specklepy.core.api.resources.stream import Resource as CoreResource
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
from specklepy.core.api.resources.stream import Resource as CoreResource
class Resource(CoreResource):
@@ -250,11 +256,7 @@ class Resource(CoreResource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.SDK,
self.account,
{"name": "Stream Permission Update", "role": role},
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Update", "role": role})
return super().update_permission(stream_id, user_id, role)
def revoke_permission(self, stream_id: str, user_id: str):
@@ -299,3 +301,4 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
+7 -8
View File
@@ -1,12 +1,15 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from gql import gql
from graphql import DocumentNode
from specklepy.core.api.resources.subscriptions import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.core.api.resources.subscriptions import Resource as CoreResource
def check_wsclient(function):
@wraps(function)
@@ -61,9 +64,7 @@ class Resource(CoreResource):
Returns:
Stream -- the update stream
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Updated"}
)
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Updated"})
return super().stream_updated(id, callback)
@check_wsclient
@@ -82,9 +83,7 @@ class Resource(CoreResource):
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Removed"}
)
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Removed"})
return super().stream_removed(callback)
@check_wsclient
+16 -11
View File
@@ -1,13 +1,17 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.core.api.resources.user import Resource as CoreResource
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
@@ -42,7 +46,7 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Get_deprecated"})
return super().get(id)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
@@ -79,7 +83,7 @@ class Resource(CoreResource):
Returns:
bool -- True if your profile was updated successfully
"""
# metrics.track(metrics.USER, self.account, {"name": "update"})
#metrics.track(metrics.USER, self.account, {"name": "update"})
metrics.track(metrics.SDK, self.account, {"name": "User Update_deprecated"})
return super().update(name, company, bio, avatar)
@@ -114,6 +118,7 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Activity_deprecated"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
@@ -125,11 +130,10 @@ class Resource(CoreResource):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(
metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"}
)
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"})
return super().get_all_pending_invites()
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
@@ -148,6 +152,7 @@ class Resource(CoreResource):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetInvite_deprecated"})
return super().get_pending_invite(stream_id, token)
+13 -7
View File
@@ -1,9 +1,17 @@
from urllib.parse import unquote, urlparse
from warnings import warn
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
from specklepy.logging import metrics
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
from specklepy.logging import metrics
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
class StreamWrapper(CoreStreamWrapper):
"""
@@ -43,7 +51,7 @@ class StreamWrapper(CoreStreamWrapper):
_account: Account = None
def __init__(self, url: str) -> None:
super().__init__(url=url)
super().__init__(url = url)
def get_account(self, token: str = None) -> Account:
"""
@@ -82,7 +90,5 @@ class StreamWrapper(CoreStreamWrapper):
ServerTransport -- constructed for this stream
with a pre-authenticated client
"""
metrics.track(
metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"}
)
metrics.track(metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"})
return super().get_transport(token)
+7 -21
View File
@@ -11,6 +11,7 @@ from gql.transport.websockets import WebsocketsTransport
from specklepy.core.api import resources
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.core.api.resources import (
user,
active_user,
branch,
commit,
@@ -19,7 +20,6 @@ from specklepy.core.api.resources import (
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -59,14 +59,7 @@ class SpeckleClient:
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
ws_protocol = "ws"
http_protocol = "http"
@@ -81,17 +74,9 @@ class SpeckleClient:
self.graphql = f"{self.url}/graphql"
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=self.connection_retries,
timeout=self.connection_timeout,
)
transport=RequestsHTTPTransport(url=self.graphql, verify=True, retries=3)
)
self.wsclient = None
@@ -130,7 +115,8 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.authenticate_with_account(get_account_from_token(token))
self.authenticate_with_token(token)
self._set_up_client()
def authenticate_with_token(self, token: str) -> None:
"""
@@ -141,7 +127,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.account = Account.from_token(token, self.url)
self.account = get_account_from_token(token, self.url)
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
@@ -164,7 +150,7 @@ class SpeckleClient:
"apollographql-client-version": metrics.HOST_APP_VERSION,
}
httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=self.verify_certificate, retries=3
url=self.graphql, headers=headers, verify=True, retries=3
)
wstransport = WebsocketsTransport(
url=self.ws_url,
+2 -24
View File
@@ -1,12 +1,12 @@
import os
from pathlib import Path
from typing import List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.core.api.models import ServerInfo
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
@@ -110,7 +110,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
if not default:
default = accounts[0]
default.isDefault = True
# metrics.initialise_tracker(default)
#metrics.initialise_tracker(default)
return default
@@ -143,28 +143,6 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
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:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
-7
View File
@@ -185,11 +185,6 @@ class ActivityCollection(BaseModel):
return self.__repr__()
class ServerMigration(BaseModel):
movedTo: Optional[str] = None
movedFrom: Optional[str] = None
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
@@ -201,5 +196,3 @@ class ServerInfo(BaseModel):
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import List, Optional
# from specklepy.logging import metrics
#from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -3,11 +3,7 @@ from typing import List, Optional
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
@@ -4,7 +4,6 @@ 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"
@@ -40,8 +39,6 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = {
"branch": {
"streamId": stream_id,
+3 -4
View File
@@ -1,10 +1,9 @@
from typing import List, Optional, Union
from typing import List, Optional
from gql import gql
from specklepy.core.api.models import Commit
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit"
@@ -107,8 +106,8 @@ class Resource(ResourceBase):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
+1 -14
View File
@@ -1,7 +1,6 @@
import re
from typing import Any, Dict, List, Tuple
import requests
from gql import gql
from specklepy.core.api.models import ServerInfo
@@ -57,21 +56,9 @@ class Resource(ResourceBase):
"""
)
server_info = self.make_request(
return 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
+11 -14
View File
@@ -1,13 +1,10 @@
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
@@ -166,8 +163,7 @@ 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}
}
@@ -508,10 +504,11 @@ class Resource(ResourceBase):
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else [])
for user_id in (user_ids if user_ids is not None else [])
if user_id is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
@@ -731,13 +728,13 @@ class Resource(ResourceBase):
"stream_id": stream_id,
"limit": limit,
"action_type": action_type,
"before": (
before.astimezone(timezone.utc).isoformat() if before else before
),
"before": before.astimezone(timezone.utc).isoformat()
if before
else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": (
cursor.astimezone(timezone.utc).isoformat() if cursor else cursor
),
"cursor": cursor.astimezone(timezone.utc).isoformat()
if cursor
else cursor,
}
except AttributeError as e:
raise SpeckleException(
+2 -5
View File
@@ -4,12 +4,9 @@ from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "user"
+27 -123
View File
@@ -1,13 +1,11 @@
from urllib.parse import quote, unquote, urlparse
from urllib.parse import unquote, urlparse
from warnings import warn
from gql import gql
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_accounts_for_server,
get_local_accounts,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -47,7 +45,6 @@ class StreamWrapper:
commit_id: str = None
object_id: str = None
branch_name: str = None
model_id: str = None
_client: SpeckleClient = None
_account: Account = None
@@ -84,86 +81,29 @@ class StreamWrapper:
" provided."
)
# 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 use_fe2 is False:
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
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)
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:
self.model_id = self.branch_name
# get branch name
query = gql(
"""
query Project($project_id: String!, $model_id: String!) {
project(id: $project_id) {
id
model(id: $model_id) {
name
}
}
}
"""
)
self._client = self.get_client()
params = {"project_id": self.stream_id, "model_id": self.model_id}
project = self._client.httpclient.execute(query, params)
try:
self.branch_name = project["project"]["model"]["name"]
except KeyError as ke:
raise SpeckleException("Project model name is not found", ke)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
@property
@@ -178,7 +118,14 @@ class StreamWrapper:
if self._account and self._account.token:
return self._account
self._account = next(iter(get_accounts_for_server(self.host)), None)
self._account = next(
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
@@ -237,46 +184,3 @@ 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 -2
View File
@@ -28,7 +28,7 @@ CONNECTOR = "Connector Action"
RECEIVE = "Receive"
SEND = "Send"
# not in use since 2.15
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
BRANCH = "Branch Action"
CLIENT = "Speckle Client"
@@ -142,7 +142,7 @@ class MetricsTracker(metaclass=Singleton):
def hash(self, value: str):
inputList = value.lower().split("://")
input = inputList[len(inputList) - 1].split("/")[0].split("?")[0]
input = inputList[len(inputList)-1].split("/")[0].split('?')[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
+4 -3
View File
@@ -1,6 +1,5 @@
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
@@ -9,7 +8,9 @@ class CRS(Base, speckle_type="Objects.GIS.CRS"):
name: Optional[str] = None
authority_id: Optional[str] = None
wkt: Optional[str] = None
units_native: Optional[str] = None
units_native: Optional[str] = None
offset_x: Optional[float] = None
offset_y: Optional[float] = None
rotation: Optional[float] = None
+14 -14
View File
@@ -1,22 +1,22 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
+13 -34
View File
@@ -1,51 +1,35 @@
from typing import List, Optional, Union
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
from typing import Optional, Union, List
from specklepy.objects.geometry import Point, Line, Polyline, Circle, Arc, Polycurve, Mesh
from specklepy.objects import Base
from deprecated import deprecated
class GisPolygonGeometry(
Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}
):
class GisPolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}):
"""GIS Polygon Geometry"""
boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None
displayValue: Optional[List[Mesh]] = None
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
geometry: Optional[List[GisPolygonGeometry]] = None
attributes: Optional[Base] = None
class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"):
"""GIS Polyline element"""
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
attributes: Optional[Base] = None
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None,
attributes: Optional[Base] = None,
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
geometry: Optional[List[Point]] = None
attributes: Optional[Base] = None
geometry: Optional[List[Point]] = None,
attributes: Optional[Base] = None,
class GisRasterElement(
Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}
):
class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}):
"""GIS Raster element"""
band_count: Optional[int] = None
@@ -59,16 +43,11 @@ class GisRasterElement(
noDataValue: Optional[List[float]] = None
displayValue: Optional[List[Mesh]] = None
class GisTopography(
GisRasterElement,
speckle_type="Objects.GIS.GisTopography",
detachable={"displayValue"},
):
class GisTopography(GisRasterElement, speckle_type="Objects.GIS.GisTopography", detachable={"displayValue"}):
"""GIS Raster element with 3d Topography representation"""
class GisNonGeometryElement(Base, speckle_type="Objects.GIS.NonGeometryElement"):
"""GIS Table feature"""
attributes: Optional[Base] = None
+24 -83
View File
@@ -1,26 +1,23 @@
from typing import Any, Dict, List, Optional, Union
from deprecated import deprecated
from typing import Any, Dict, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.other import Collection
from specklepy.objects.GIS.CRS import CRS
from deprecated import deprecated
@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead")
class Layer(Base, detachable={"features"}):
"""A GIS Layer"""
def __init__(
self,
name: Optional[str] = None,
crs: Optional[CRS] = None,
name:str=None,
crs:CRS=None,
units: str = "m",
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[Dict[str, Any]] = None,
**kwargs,
renderer: Optional[dict[str, Any]] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name
@@ -29,27 +26,25 @@ class Layer(Base, detachable={"features"}):
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
self.renderer = renderer or {}
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="VectorLayer",
serialize_ignore={"features"},
):
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"}):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
name: Optional[str]=None
crs: Optional[CRS]=None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -59,25 +54,24 @@ class VectorLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"},
):
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"}):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
crs: Optional[CRS]=None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
rasterCrs: Optional[CRS]=None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -87,56 +81,3 @@ class RasterLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class VectorLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+2 -19
View File
@@ -1,23 +1,6 @@
"""Builtin Speckle object kit."""
from specklepy.objects import (
GIS,
encoding,
geometry,
other,
primitive,
structural,
units,
)
from specklepy.objects import encoding, geometry, other, primitive, structural, units
from specklepy.objects.base import Base
__all__ = [
"Base",
"encoding",
"geometry",
"other",
"units",
"structural",
"primitive",
"GIS",
]
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
+2 -3
View File
@@ -19,7 +19,7 @@ from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.objects.units import Units, get_units_from_string
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -188,8 +188,7 @@ class _RegisteringBase:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
# we know, that the super here is object, that takes no args on init subclass
return super().__init_subclass__()
super().__init_subclass__(**kwargs)
# T = TypeVar("T")
+14 -14
View File
@@ -40,9 +40,9 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
class Pointcloud(
Base,
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
@@ -303,15 +303,15 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
class SpiralType(Enum):
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Biquadratic = (0,)
BiquadraticParabola = (1,)
Bloss = (2,)
Clothoid = (3,)
Cosine = (4,)
Cubic = (5,)
CubicParabola = (6,)
Radioid = (7,)
Sinusoid = (8,)
Unknown = 9
@@ -319,7 +319,7 @@ class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[float]
turns: Optional[int]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
@@ -898,7 +898,7 @@ class Brep(
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
encoded_unit = get_encoding_from_units(self.Vertices[0]._units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
@@ -913,7 +913,7 @@ class Brep(
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex.units = units
vertex._units = units
vertices.append(vertex)
self.Vertices = vertices
@@ -1,25 +1,24 @@
from abc import ABC, abstractmethod
from typing import Any, Collection, Dict, Generic, Iterable, Optional, Tuple, TypeVar
from dataclasses import dataclass
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
from attrs import define
from specklepy.objects.base import Base
ROOT: str = "__Root"
T = TypeVar("T")
T = TypeVar('T')
PARENT_INFO = Tuple[Optional[str], str]
@define(slots=True)
class CommitObjectBuilder(ABC, Generic[T]):
converted: Dict[str, Base]
_parent_infos: Dict[str, Collection[PARENT_INFO]]
def __init__(self) -> None:
self.converted = {}
self._parent_infos = {}
@abstractmethod
def include_object(self, conversion_result: Base, native_object: T) -> None:
pass
@@ -27,17 +26,14 @@ class CommitObjectBuilder(ABC, Generic[T]):
def build_commit_object(self, root_commit_object: Base) -> None:
self.apply_relationships(self.converted.values(), root_commit_object)
def set_relationship(
self, app_id: Optional[str], *parent_info: PARENT_INFO
) -> None:
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
if not app_id:
return
self._parent_infos[app_id] = parent_info
def apply_relationships(
self, to_add: Iterable[Base], root_commit_object: Base
) -> None:
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
for c in to_add:
try:
self.apply_relationship(c, root_commit_object)
@@ -45,25 +41,20 @@ class CommitObjectBuilder(ABC, Generic[T]):
print(f"Failed to add object {type(c)} to commit object: {ex}")
def apply_relationship(self, current: Base, root_commit_object: Base):
if not current.applicationId:
raise Exception("Expected applicationId to have been set")
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
parents = self._parent_infos[current.applicationId]
for parent_id, prop_name in parents:
if not parent_id:
continue
for (parent_id, prop_name) in parents:
if not parent_id: continue
parent: Optional[Base]
if parent_id == ROOT:
parent = root_commit_object
else:
parent = (
self.converted[parent_id] if parent_id in self.converted else None
)
if not parent:
continue
parent = self.converted[parent_id] if parent_id in self.converted else None
if not parent: continue
try:
elements = get_detached_prop(parent, prop_name)
@@ -75,26 +66,18 @@ class CommitObjectBuilder(ABC, Generic[T]):
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
print(
f"Failed to add object {type(current)} to a converted parent; {ex}"
)
raise Exception(
f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!"
)
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
return getattr(speckle_object, detached_prop_name, None)
def set_detached_prop(
speckle_object: Base, prop_name: str, value: Optional[Any]
) -> None:
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
setattr(speckle_object, detached_prop_name, value)
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
@@ -3,7 +3,7 @@ from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional
from attrs import define
from typing_extensions import Protocol, final
from specklepy.objects.base import Base
from specklepy.objects import Base
class ITraversalRule(Protocol):
@@ -41,6 +41,7 @@ class TraversalContext:
@final
@define(slots=True, frozen=True)
class GraphTraversal:
_rules: List[ITraversalRule]
def traverse(self, root: Base) -> Iterator[TraversalContext]:
@@ -57,15 +58,15 @@ class GraphTraversal:
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}:
continue # debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if getattr(current, child_prop, None):
value = current[child_prop]
self._traverse_member_to_stack(stack, value, child_prop, head)
except KeyError:
self._traverse_member_to_stack(
stack, value, child_prop, head
)
except KeyError as ex:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
def _traverse_member_to_stack(
stack: List[TraversalContext],
@@ -77,14 +78,10 @@ class GraphTraversal:
stack.append(TraversalContext(value, member_name, parent))
elif isinstance(value, list):
for obj in value:
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
elif isinstance(value, dict):
for obj in value.values():
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
@staticmethod
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
@@ -99,6 +96,7 @@ class GraphTraversal:
for o in GraphTraversal.traverse_member(obj):
yield o
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
return self._get_active_rule(o) or _default_rule
@@ -122,4 +120,4 @@ class TraversalRule:
for condition in self._conditions:
if condition(o):
return True
return False
return False
+10 -23
View File
@@ -1,8 +1,7 @@
from typing import Any, List, Optional
from deprecated import deprecated
from specklepy.objects.geometry import Plane, Point, Polyline, Vector
from specklepy.objects.geometry import Point, Vector
from .base import Base
@@ -72,19 +71,6 @@ class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
lineweight: float = 0
class Text(Base, speckle_type=OTHER + "Text"):
"""
Text object to render it on viewer.
"""
plane: Plane
value: str
height: float
rotation: float
displayValue: Optional[List[Polyline]] = None
richText: Optional[str] = None
class Transform(
Base,
speckle_type=OTHER + "Transform",
@@ -261,7 +247,9 @@ class BlockDefinition(
geometry: Optional[List[Base]] = None
class Instance(Base, speckle_type=OTHER + "Instance", detachable={"definition"}):
class Instance(
Base, speckle_type=OTHER + "Instance", detachable={"definition"}
):
transform: Optional[Transform] = None
definition: Optional[Base] = None
@@ -280,17 +268,17 @@ class BlockInstance(
def blockDefinition(self, value: Optional[BlockDefinition]) -> None:
self.definition = value
class RevitInstance(Instance, speckle_type=OTHER_REVIT + "RevitInstance"):
level: Optional[Base] = None
level: Optional[Base] = None
facingFlipped: bool
handFlipped: bool
parameters: Optional[Base] = None
parameters: Optional[Base] = None
elementId: Optional[str]
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
class RevitParameter(
Base, speckle_type="Objects.BuiltElements.Revit.Parameter"
):
name: Optional[str] = None
value: Any = None
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
@@ -302,10 +290,9 @@ class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter")
isReadOnly: bool = False
isTypeParameter: bool = False
class Collection(
Base, speckle_type="Speckle.Core.Models.Collection", detachable={"elements"}
):
name: Optional[str] = None
collectionType: Optional[str] = None
elements: Optional[List[Base]] = None
elements: Optional[List[Base]] = None
+4 -1
View File
@@ -6,7 +6,10 @@ from specklepy.objects.structural.analysis import (
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import Axis, AxisType
from specklepy.objects.structural.axis import (
AxisType,
Axis
)
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
+1 -1
View File
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Optional
from enum import Enum
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
+2 -4
View File
@@ -106,9 +106,7 @@ def get_encoding_from_units(unit: Union[Units, str, None]):
def get_scale_factor_from_string(fromUnits: str, toUnits: str) -> float:
"""Returns a scalar to convert distance values from one unit system to another"""
return get_scale_factor(
get_units_from_string(fromUnits), get_units_from_string(toUnits)
)
return get_scale_factor(get_units_from_string(fromUnits), get_units_from_string(toUnits))
def get_scale_factor(fromUnits: Units, toUnits: Units) -> float:
@@ -121,4 +119,4 @@ def get_scale_factor_to_meters(fromUnits: Units) -> float:
if fromUnits not in UNIT_SCALE:
raise ValueError(f"Invalid units provided: {fromUnits}")
return UNIT_SCALE[fromUnits]
return UNIT_SCALE[fromUnits]
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Any, Dict, List
from specklepy.transports.abstract_transport import AbstractTransport
@@ -137,7 +137,7 @@ class BatchSender(object):
raise SpeckleException(
message=(
"Could not save the object to the server - status code"
f" {r.status_code} ({r.text[:1000]})"
f" {r.status_code} ({r.text[:1000]})"
)
)
except json.JSONDecodeError as error:
+9 -13
View File
@@ -1,5 +1,5 @@
import json
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
from warnings import warn
import requests
@@ -73,7 +73,7 @@ class ServerTransport(AbstractTransport):
warn(
SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport"
f" for {url}. Receiving from private streams will fail."
f" for {self.url}. Receiving from private streams will fail."
)
)
else:
@@ -84,18 +84,14 @@ class ServerTransport(AbstractTransport):
self.stream_id = stream_id
self.url = url
self.session = requests.Session()
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
if self.account is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
)
@property
def name(self) -> str:
+1 -1
View File
@@ -1,7 +1,7 @@
import os
import sqlite3
from contextlib import closing
from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
@@ -1,259 +0,0 @@
"""Run integration tests with a speckle server."""
import os
from pathlib import Path
from typing import Dict
import pytest
from gql import gql
from speckle_automate import (
AutomationContext,
AutomationRunData,
AutomationStatus,
run_function,
)
from speckle_automate.fixtures import (
create_test_automation_run_data,
crypto_random_string,
)
from speckle_automate.schema import AutomateBase
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
@pytest.fixture
def speckle_token(user_dict: Dict[str, str]) -> str:
"""Provide a speckle token for the test suite."""
return user_dict["token"]
@pytest.fixture
def speckle_server_url(host: str) -> str:
"""Provide a speckle server url for the test suite, default to localhost."""
return f"http://{host}"
@pytest.fixture
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
test_client = SpeckleClient(speckle_server_url, use_ssl=False)
test_client.authenticate_with_token(speckle_token)
return test_client
@pytest.fixture
def automation_run_data(
test_client: SpeckleClient, speckle_server_url: str
) -> AutomationRunData:
"""TODO: Set up a test automation for integration testing"""
project_id = crypto_random_string(10)
test_automation_id = crypto_random_string(10)
return create_test_automation_run_data(
test_client, speckle_server_url, project_id, test_automation_id
)
@pytest.fixture
def automation_context(
automation_run_data: AutomationRunData, speckle_token: str
) -> AutomationContext:
"""Set up the run context."""
return AutomationContext.initialize(automation_run_data, speckle_token)
def get_automation_status(
project_id: str,
model_id: str,
speckle_client: SpeckleClient,
):
query = gql(
"""
query AutomationRuns(
$projectId: String!
$modelId: String!
)
{
project(id: $projectId) {
model(id: $modelId) {
automationStatus {
id
status
statusMessage
automationRuns {
id
automationId
versionId
createdAt
updatedAt
status
functionRuns {
id
functionId
elapsed
status
contextView
statusMessage
results
resultVersions {
id
}
}
}
}
}
}
}
"""
)
params = {
"projectId": project_id,
"modelId": model_id,
}
response = speckle_client.httpclient.execute(query, params)
return response["project"]["model"]["automationStatus"]
class FunctionInputs(AutomateBase):
forbidden_speckle_type: str
def automate_function(
automation_context: AutomationContext,
function_inputs: FunctionInputs,
) -> None:
"""Hey, trying the automate sdk experience here."""
version_root_object = automation_context.receive_version()
count = 0
if version_root_object.speckle_type == function_inputs.forbidden_speckle_type:
if not version_root_object.id:
raise ValueError("Cannot operate on objects without their id's.")
automation_context.attach_error_to_objects(
"Forbidden speckle_type",
version_root_object.id,
"This project should not contain the type: "
f"{function_inputs.forbidden_speckle_type}",
)
count += 1
if count > 0:
automation_context.mark_run_failed(
"Automation failed: "
f"Found {count} object that have a forbidden speckle type: "
f"{function_inputs.forbidden_speckle_type}"
)
else:
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(
automation_context,
automate_function,
FunctionInputs(forbidden_speckle_type="Base"),
)
assert automation_context.run_status == AutomationStatus.FAILED
status = get_automation_status(
automation_context.automation_run_data.project_id,
automation_context.automation_run_data.model_id,
automation_context.speckle_client,
)
assert status["status"] == automation_context.run_status
status_message = status["automationRuns"][0]["functionRuns"][0]["statusMessage"]
assert status_message == automation_context.status_message
@pytest.fixture
def test_file_path():
path = Path(f"./{crypto_random_string(10)}").resolve()
yield 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
):
"""Test file store capabilities of the automate sdk."""
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
test_file_path.write_text("foobar")
automation_context.store_file_result(test_file_path)
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:
with pytest.raises(ValueError):
automation_context.create_new_version_in_project(
Base(), automation_context.automation_run_data.branch_name
)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project(
automation_context: AutomationContext,
) -> None:
root_object = Base()
root_object.foo = "bar"
model_id, version_id = automation_context.create_new_version_in_project(
root_object, "foobar"
)
assert model_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:
automation_context.set_context_view()
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id}"
)
automation_context.report_run_status()
automation_context._automation_result.result_view = None
dummy_context = "foo@bar"
automation_context.set_context_view([dummy_context])
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id},{dummy_context}"
)
automation_context.report_run_status()
automation_context._automation_result.result_view = None
dummy_context = "foo@baz"
automation_context.set_context_view(
[dummy_context], include_source_model_version=False
)
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(f"models/{dummy_context}")
automation_context.report_run_status()
@@ -18,7 +18,6 @@ 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()
@@ -8,7 +8,11 @@ from specklepy.api.models import (
Stream,
User,
)
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
)
from devtools import debug
@pytest.mark.run(order=3)
@@ -44,14 +48,6 @@ class TestStream:
assert isinstance(stream.id, str)
def test_stream_create_short_name(self, client, stream, updated_stream):
new_stream_id = client.stream.create(
name="x",
description=stream.description,
is_public=stream.isPublic,
)
assert isinstance(new_stream_id, SpeckleException)
def test_stream_get(self, client, stream):
fetched_stream = client.stream.get(stream.id)
@@ -205,6 +201,9 @@ class TestStream:
def test_stream_activity(self, client: SpeckleClient, stream: Stream):
activity = client.stream.activity(stream.id)
if isinstance(activity, SpeckleException):
debug(activity.with_traceback())
older_activity = client.stream.activity(
stream.id, before=activity.items[0].time
)
@@ -2,13 +2,11 @@ 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)
@@ -31,22 +29,6 @@ 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"
@@ -144,72 +126,3 @@ def test_wrapper_url_match(user_path) -> None:
account = wrap.get_account()
assert account.userInfo.email is None
def test_parse_project():
wrap = StreamWrapper("https://latest.speckle.systems/projects/843d07eb10")
assert wrap.type == "stream"
def test_parse_model():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/d9eb4918c8"
)
assert wrap.branch_name == "building wrapper"
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)
@@ -1,77 +0,0 @@
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)
+3 -3
View File
@@ -388,9 +388,9 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
def test_brep_vertices_values_serialization():
brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, units=Units.mm).get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, units=Units.mm).get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, units=Units.mm).get_id()
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units=Units.mm).get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units=Units.mm).get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units=Units.mm).get_id()
def test_trims_value_serialization():
+1 -1
View File
@@ -102,4 +102,4 @@ class GraphTraversalTests(TestCase):
self.assertCountEqual(ret, [test_case, expected_traverse, expected_traverse])
self.assertNotIn(expected_ignore, ret)
self.assertEqual(len(ret), 3)
self.assertEqual(len(ret), 3)
+1 -6
View File
@@ -107,12 +107,7 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
fake_bases,
),
(List["int"], [2, 3, 4], True, [2, 3, 4]),
(
Union[float, Dict[str, float]],
{"foo": 1, "bar": 2},
True,
{"foo": 1.0, "bar": 2.0},
),
(Union[float, Dict[str, float]], {"foo": 1, "bar": 2}, True, {"foo": 1.0, "bar": 2.0}),
(Union[float, Dict[str, float]], {"foo": "bar"}, False, {"foo": "bar"}),
],
)
+13 -9
View File
@@ -1,3 +1,5 @@
import pytest
from specklepy.objects.units import Units, get_scale_factor
@@ -6,7 +8,7 @@ from specklepy.objects.units import Units, get_scale_factor
@pytest.mark.parametrize(
"fromUnits, toUnits, inValue, expectedOutValue",
[
# To self
#To self
(Units.km, Units.km, 1.5, 1.5),
(Units.km, Units.km, 0, 0),
(Units.m, Units.m, 1.5, 1.5),
@@ -21,20 +23,24 @@ from specklepy.objects.units import Units, get_scale_factor
(Units.yards, Units.yards, 0, 0),
(Units.feet, Units.feet, 1.5, 1.5),
(Units.feet, Units.feet, 0, 0),
# To Meters
#To Meters
(Units.km, Units.m, 987654.321, 987654321),
(Units.m, Units.m, 987654.321, 987654.321),
(Units.mm, Units.m, 98765432.1, 98765.4321),
(Units.cm, Units.m, 9876543.21, 98765.4321),
# To negative meters
#To negative meters
(Units.km, Units.m, -987654.321, -987654321),
(Units.m, Units.m, -987654.321, -987654.321),
(Units.m, Units.m,- 987654.321, -987654.321),
(Units.mm, Units.m, -98765432.1, -98765.4321),
(Units.cm, Units.m, -9876543.21, -98765.4321),
(Units.m, Units.km, 987654.321, 987.654321),
(Units.m, Units.cm, 987654.321, 98765432.1),
(Units.m, Units.mm, 987654.321, 987654321),
# Imperial
#Imperial
(Units.miles, Units.m, 123.45, 198673.517),
(Units.miles, Units.inches, 123.45, 7821792),
(Units.yards, Units.m, 123.45, 112.88268),
@@ -44,9 +50,7 @@ from specklepy.objects.units import Units, get_scale_factor
(Units.inches, Units.m, 123.45, 3.13563),
],
)
def test_get_scale_factor_between_units(
fromUnits: Units, toUnits: Units, inValue: float, expectedOutValue: float
):
def test_get_scale_factor_between_units(fromUnits: Units, toUnits: Units, inValue: float, expectedOutValue: float):
Tolerance = 1e-10
actual = inValue * get_scale_factor(fromUnits, toUnits)
assert actual - expectedOutValue < Tolerance
assert(actual - expectedOutValue < Tolerance)
+19 -14
View File
@@ -3,9 +3,9 @@ Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from importlib import import_module, invalidate_caches
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,7 +55,9 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception("Cannot get appdata path from environment.")
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
@@ -66,7 +68,9 @@ def user_application_data_path() -> Path:
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception("Failed to initialize user application data path.", ex)
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
@@ -86,16 +90,19 @@ def user_speckle_connector_installation_path(host_application: str) -> Path:
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
@@ -106,6 +113,7 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -124,9 +132,7 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
@@ -178,7 +184,7 @@ def _import_dependencies() -> None:
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
@@ -189,7 +195,6 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -197,6 +202,6 @@ def ensure_dependencies(host_application: str) -> None:
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")