Compare commits

..

37 Commits

Author SHA1 Message Date
KatKatKateryna 2f7113f0c5 linter 2024-08-04 23:25:43 +01:00
KatKatKateryna 7f38b24421 isort 2024-08-04 22:52:56 +01:00
KatKatKateryna cac4551fdf extension 2024-08-04 21:50:22 +01:00
KatKatKateryna 813bc8be4d gisfeature class 2024-08-04 21:44:29 +01:00
KatKatKateryna a45ae03235 return deprecated command 2024-08-04 21:42:28 +01:00
Iain Sproat 905377dea1 feat(default domain): app.speckle.systems is now default over speckle.xyz (#343)
- also updates the example email domain to use the IANA owned example domain instead of a production or random domain
2024-07-18 17:19:32 +02:00
Gergő Jedlicska 62c5114cb3 Merge pull request #341 from specklesystems/gergo/fixtures_no_init
fix: remove fixtures from automate exports
2024-06-07 18:53:20 +02:00
Gergő Jedlicska 43a5302a90 fix: tures 2024-06-07 18:51:03 +02:00
Gergő Jedlicska addaa996ea fix: remove fixtures from automate exports 2024-06-07 18:42:19 +02:00
Gergő Jedlicska 3b5421a5bc Merge pull request #340 from specklesystems/gergo/automateExceptionOutcome
feat: add excetion outcome reporting to functions
2024-06-07 15:29:40 +02:00
Gergő Jedlicska 88e8c86fa6 feat: add excetion outcome reporting to functions 2024-06-07 11:13:15 +02:00
Chuck Driesler d6843b9971 Merge pull request #339 from specklesystems/chuck/testAutomationHelpers
WEB-1053 Create helpers for testing automate functions
2024-06-06 12:03:00 +01:00
Charles Driesler 302a9f7f30 repair import 2024-06-06 12:01:00 +01:00
Charles Driesler ede9591c6a export fixtures 2024-06-06 11:58:18 +01:00
Charles Driesler c5b339d891 deps deps deps 2024-06-05 16:59:05 +01:00
Charles Driesler 2e35fb9e5c create helpers for testing functions 2024-06-05 16:38:49 +01:00
Gergő Jedlicska e6b822b0e3 Merge pull request #338 from specklesystems/gergo/automateExitCode
fix(automate): make sure we exit with code 0 if execution completes
2024-06-03 16:08:28 +02:00
Gergő Jedlicska 239bc4b5b9 docs(automate): finish comment block thoughts 2024-06-03 14:29:29 +02:00
Gergő Jedlicska 4eea15ddc1 fix(automate): make sure we exit with code 0 if execution completes 2024-06-03 14:27:07 +02:00
Aleksei Protopopov 204aa7466e Feature: adds connection_timeout argument to SpeckleClient (#337)
* Add connection_timeout argument to SpeckleClient

* Reformat code with black

* Set default timeout to 10s

* Make connection retries configurable
2024-05-27 14:23:39 +01:00
Gergő Jedlicska 24019e99f3 Merge pull request #335 from specklesystems/gergo/automateInterfaceRework
Rework automate SDK for the integrated automate api
2024-05-16 18:14:47 +02:00
Gergő Jedlicska 64492fafa5 fix: proper pytest skip 2024-05-16 17:24:53 +02:00
Gergő Jedlicska 3a8d634989 test: disable automation tests for now 2024-05-16 17:18:57 +02:00
Gergő Jedlicska f27650af3a feat: update automation schema and automation context for the new automate interfaces 2024-05-16 10:25:58 +02:00
KatKatKateryna 6469b6f757 Merge pull request #334 from specklesystems/jsdb/doc-strings-patch
Corrects and enhances user API class documentation (CNX-9172)
2024-03-28 23:42:09 +08:00
KatKatKateryna b28db0881c formatting 2024-03-28 16:22:21 +01:00
Jonathon Broughton b0b442de23 fix poetry in dockerfile 2024-03-28 14:48:34 +00:00
Jonathon Broughton 32d2fe8ead Merge branch 'main' into jsdb/doc-strings-patch 2024-03-26 12:48:45 +00:00
Jonathon Broughton 9fd40eac23 Update other_user.py 2024-03-26 12:43:14 +00:00
Benjamin Ottensten b22ba1f1f1 Update web app link in the README (#333) 2024-03-26 12:39:59 +00:00
Jonathon Broughton 5e20fe7bf1 Corrected/updated docstrings for method signatures 2024-03-26 11:06:35 +00:00
Jedd Morgan 6da5da23c4 feat(core): [CNX-9108] Added server migration support (#331)
* Added server migration support

* fix obvious mistake

* Fixed slightly less obvious mistake

* Run black

* isort
2024-03-25 11:15:41 +00:00
Gergő Jedlicska 1b59f0b026 feat: fix authenticate with token mechanism (#330) 2024-02-26 16:30:28 +00:00
Jedd Morgan 78123936d2 Merge pull request #329 from specklesystems/jrm/spirals-fix
fix(objects): [CNX-9014] Fixed issue with Spiral turns not deserializing in SpecklePy
2024-02-19 13:05:38 +00:00
Jedd Morgan dbc1aefed3 Fixed issue with Spiral turns not deserializing in SpecklePy 2024-02-19 12:41:35 +00:00
Gergő Jedlicska e726345b0c Merge pull request #328 from mortenengen/fix-gis-dict-type
fix: dict type of renderer in Layer
2024-02-13 08:30:48 +01:00
Morten Engen e074dbcced gis/layers: fix dict type of renderer in Layer 2024-02-12 20:54:38 +01:00
27 changed files with 1258 additions and 1031 deletions
+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://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH=$PATH:$HOME/.poetry/env
+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 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
- [![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
- [![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
Generated
+761 -744
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -33,10 +33,11 @@ pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.0.187"
ruff = "^0.4.4"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
+1
View File
@@ -1,4 +1,5 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
+47 -61
View File
@@ -1,4 +1,5 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
@@ -17,6 +18,7 @@ from speckle_automate.schema import (
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
@@ -94,8 +96,10 @@ class AutomationContext:
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, self.automation_run_data.version_id
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
@@ -104,7 +108,7 @@ class AutomationContext:
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {self.automation_run_data.version_id}",
f" the speckle version {version_id}",
)
return base
@@ -119,19 +123,27 @@ class AutomationContext:
version_message (str): The message for the new version.
"""
if model_name == self.automation_run_data.branch_name:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {self.automation_run_data.model_id} /"
f" {self.automation_run_data.branch_name}"
)
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
# we just check if it exists
if (not branch) or isinstance(branch, SpeckleException):
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
@@ -139,8 +151,6 @@ class AutomationContext:
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
else:
model_id = branch.id
root_object_id = operations.send(
root_object,
@@ -174,7 +184,8 @@ class AutomationContext:
) -> None:
link_resources = (
[
f"{self.automation_run_data.model_id}@{self.automation_run_data.version_id}"
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
@@ -194,47 +205,26 @@ class AutomationContext:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation ReportFunctionRunStatus(
$automationId: String!,
$automationRevisionId: String!,
$automationRunId: String!,
$versionId: String!,
$functionId: String!,
$functionName: String!,
$functionLogo: String,
$runStatus: AutomationRunStatus!
$elapsed: Float!
$contextView: String
$resultVersionIds: [String!]!
mutation AutomateFunctionRunStatusReport(
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$objectResults: JSONObject
$results: JSONObject
$contextView: String
){
automationMutations {
functionRunStatusReport(input: {
automationId: $automationId
automationRevisionId: $automationRevisionId
automationRunId: $automationRunId
versionId: $versionId
functionRuns: [
{
functionId: $functionId
functionName: $functionName
functionLogo: $functionLogo
status: $runStatus,
contextView: $contextView,
elapsed: $elapsed,
resultVersionIds: $resultVersionIds,
statusMessage: $statusMessage
results: $objectResults
}]
})
}
automateFunctionRunStatusReport(input: {
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": "1.0.0",
"version": 1,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
@@ -246,19 +236,11 @@ class AutomationContext:
object_results = None
params = {
"automationId": self.automation_run_data.automation_id,
"automationRevisionId": self.automation_run_data.automation_revision_id,
"automationRunId": self.automation_run_data.automation_run_id,
"versionId": self.automation_run_data.version_id,
"functionId": self.automation_run_data.function_id,
"functionName": self.automation_run_data.function_name,
"functionLogo": self.automation_run_data.function_logo,
"runStatus": self.run_status.value,
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
"results": object_results,
"contextView": self._automation_result.result_view,
"elapsed": self.elapsed(),
"resultVersionIds": self._automation_result.result_versions,
"objectResults": object_results,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
@@ -308,6 +290,10 @@ class AutomationContext:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.FAILED, status_message)
def mark_run_exception(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.EXCEPTION, status_message)
def mark_run_success(self, status_message: Optional[str]) -> None:
"""Mark the current run a success with an optional message."""
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
+154
View File
@@ -0,0 +1,154 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
import pytest
from gql import gql
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from speckle_automate.schema import AutomationRunData, TestAutomationRunData
from specklepy.api.client import SpeckleClient
class TestAutomationEnvironment(BaseSettings):
"""Get known environment variables from local `.env` file"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="speckle_",
extra="ignore",
)
token: str = Field()
server_url: str = Field()
project_id: str = Field()
automation_id: str = Field()
@pytest.fixture()
def test_automation_environment() -> TestAutomationEnvironment:
return TestAutomationEnvironment()
@pytest.fixture()
def test_automation_token(
test_automation_environment: TestAutomationEnvironment,
) -> str:
"""Provide a speckle token for the test suite."""
return test_automation_environment.token
@pytest.fixture()
def speckle_client(
test_automation_environment: TestAutomationEnvironment,
) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
speckle_client = SpeckleClient(
test_automation_environment.server_url,
test_automation_environment.server_url.startswith("https"),
)
speckle_client.authenticate_with_token(test_automation_environment.token)
return speckle_client
def create_test_automation_run(
speckle_client: SpeckleClient, project_id: str, test_automation_id: str
) -> TestAutomationRunData:
"""Create test run to report local test results to"""
query = gql(
"""
mutation CreateTestRun(
$projectId: ID!,
$automationId: ID!
) {
projectMutations {
automationMutations(projectId: $projectId) {
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
triggers {
payload {
modelId
versionId
}
triggerType
}
}
}
}
}
"""
)
params = {"automationId": test_automation_id, "projectId": project_id}
result = speckle_client.httpclient.execute(query, params)
print(result)
return (
result.get("projectMutations")
.get("automationMutations")
.get("createTestAutomationRun")
)
@pytest.fixture()
def test_automation_run(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> TestAutomationRunData:
return create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
def create_test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
"""Create automation run data for a new run for a given test automation"""
test_automation_run_data = create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
return AutomationRunData(
project_id=test_automation_environment.project_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data["automationRunId"],
function_run_id=test_automation_run_data["functionRunId"],
triggers=test_automation_run_data["triggers"],
)
@pytest.fixture()
def test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
return create_test_automation_run_data(speckle_client, test_automation_environment)
def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
__all__ = [
"test_automation_environment",
"test_automation_token",
"speckle_client",
"test_automation_run",
"test_automation_run_data",
]
-55
View File
@@ -1,55 +0,0 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
from gql import gql
from specklepy.api.client import SpeckleClient
def register_new_automation(
speckle_client: SpeckleClient,
project_id: str,
model_id: str,
automation_id: str,
automation_name: str,
automation_revision_id: str,
) -> bool:
"""Register a new automation in the speckle server."""
query = gql(
"""
mutation CreateAutomation(
$projectId: String!
$modelId: String!
$automationName: String!
$automationId: String!
$automationRevisionId: String!
) {
automationMutations {
create(
input: {
projectId: $projectId
modelId: $modelId
automationName: $automationName
automationId: $automationId
automationRevisionId: $automationRevisionId
}
)
}
}
"""
)
params = {
"projectId": project_id,
"modelId": model_id,
"automationName": automation_name,
"automationId": automation_id,
"automationRevisionId": automation_revision_id,
}
return speckle_client.httpclient.execute(query, params)
def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
+9 -3
View File
@@ -3,6 +3,7 @@
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import sys
import traceback
@@ -65,7 +66,9 @@ def execute_automate_function(
@overload
def execute_automate_function(automate_function: AutomateFunctionWithoutInputs) -> None:
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
@@ -127,8 +130,10 @@ def execute_automate_function(
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit_code = (
0 if automation_context.run_status == AutomationStatus.SUCCEEDED else 1
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
@@ -173,6 +178,7 @@ def run_function(
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
@@ -181,7 +187,7 @@ def run_function(
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_failed(
automation_context.mark_run_exception(
"Function error. Check the automation run logs for details."
)
finally:
+32 -9
View File
@@ -1,6 +1,7 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
@@ -12,22 +13,43 @@ class AutomateBase(BaseModel):
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
"""Represents the version creation trigger payload."""
model_id: str
version_id: str
class VersionCreationTrigger(AutomateBase):
"""Represents a single version creation trigger for the automation run."""
trigger_type: Literal["versionCreation"]
payload: VersionCreationTriggerPayload
class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
model_id: str
branch_name: str
version_id: str
speckle_server_url: str
automation_id: str
automation_revision_id: str
automation_run_id: str
function_run_id: str
function_id: str
function_name: str
function_logo: Optional[str]
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class TestAutomationRunData(BaseModel):
"""Values of the run created in the test automation for local test results."""
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
@@ -41,6 +63,7 @@ class AutomationStatus(str, Enum):
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
+3 -3
View File
@@ -21,7 +21,7 @@ class SpeckleClient(CoreSpeckleClient):
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -32,7 +32,7 @@ class SpeckleClient(CoreSpeckleClient):
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -47,7 +47,7 @@ class SpeckleClient(CoreSpeckleClient):
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
USE_SSL = True
def __init__(
+26 -38
View File
@@ -7,7 +7,8 @@ from specklepy.logging import metrics
class Resource(CoreResource):
"""API Access class for users"""
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -19,13 +20,9 @@ class Resource(CoreResource):
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
"""Gets the profile of the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
@@ -41,11 +38,11 @@ class Resource(CoreResource):
):
"""Updates your user profile. All arguments are optional.
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
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.
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
@@ -62,35 +59,30 @@ class Resource(CoreResource):
cursor: Optional[datetime] = None,
):
"""
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).
Fetches collection the current authenticated user's activity
as filtered by given parameters
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
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
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.
"""
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]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
@@ -100,18 +92,14 @@ class Resource(CoreResource):
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""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.
"""Fetches a specific pending invite for the current user on a given stream.
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)
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).
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
+5 -2
View File
@@ -1,8 +1,9 @@
from typing import Optional
from typing import Optional, Union
from specklepy.api.models import Branch
from specklepy.core.api.resources.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -31,7 +32,9 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return super().create(stream_id, name, description)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
def get(
self, stream_id: str, name: str, commits_limit: int = 10
) -> Union[Branch, None, SpeckleException]:
"""Get a branch by name from a stream
Arguments:
+30 -25
View File
@@ -8,7 +8,11 @@ from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for other users, that are not the currently active user."""
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -21,13 +25,13 @@ class Resource(CoreResource):
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Retrieves the profile of a user specified by their user ID.
Arguments:
id {str} -- the user id
Args:
id (str): The unique identifier of the user.
Returns:
LimitedUser -- the retrieved profile of another user
LimitedUser: The profile of the user with limited information.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
@@ -35,18 +39,21 @@ class Resource(CoreResource):
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
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"})
@@ -62,21 +69,19 @@ class Resource(CoreResource):
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
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.
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
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
+1 -1
View File
@@ -22,7 +22,7 @@ class StreamWrapper(CoreStreamWrapper):
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
+13 -7
View File
@@ -30,7 +30,7 @@ class SpeckleClient:
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -41,7 +41,7 @@ class SpeckleClient:
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -56,7 +56,7 @@ class SpeckleClient:
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
USE_SSL = True
def __init__(
@@ -64,6 +64,8 @@ class SpeckleClient:
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
ws_protocol = "ws"
http_protocol = "http"
@@ -80,10 +82,15 @@ class SpeckleClient:
self.ws_url = f"{ws_protocol}://{host}/graphql"
self.account = Account()
self.verify_certificate = verify_certificate
self.connection_retries = connection_retries
self.connection_timeout = connection_timeout
self.httpclient = Client(
transport=RequestsHTTPTransport(
url=self.graphql, verify=self.verify_certificate, retries=3
url=self.graphql,
verify=self.verify_certificate,
retries=self.connection_retries,
timeout=self.connection_timeout,
)
)
self.wsclient = None
@@ -123,8 +130,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.authenticate_with_token(token)
self._set_up_client()
self.authenticate_with_account(get_account_from_token(token))
def authenticate_with_token(self, token: str) -> None:
"""
@@ -135,7 +141,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.account = get_account_from_token(token, self.url)
self.account = Account.from_token(token, self.url)
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
+25 -2
View File
@@ -1,6 +1,7 @@
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
@@ -57,7 +58,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.model_validate_json(r[1]) for r in res)
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
@@ -78,7 +79,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
if json_acct_files:
try:
accounts.extend(
Account.model_validate_json(Path(json_path, json_file).read_text())
Account.parse_raw(Path(json_path, json_file).read_text())
# Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files
)
@@ -142,6 +143,28 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
return Account.from_token(token, server_url)
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(
+6
View File
@@ -185,6 +185,11 @@ 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
@@ -197,3 +202,4 @@ class ServerInfo(BaseModel):
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
+3 -10
View File
@@ -7,7 +7,7 @@ from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
get_accounts_for_server,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -30,7 +30,7 @@ class StreamWrapper:
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -178,14 +178,7 @@ class StreamWrapper:
if self._account and self._account.token:
return self._account
self._account = next(
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
self._account = next(iter(get_accounts_for_server(self.host)), None)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
+13
View File
@@ -0,0 +1,13 @@
from typing import List, Optional
from specklepy.objects.base import Base
class GisFeature(
Base, speckle_type="Objects.GIS.GisFeature", detachable={"displayValue"}
):
"""GIS Feature"""
geometry: Optional[List[Base]] = None
attributes: Base
displayValue: Optional[List[Base]] = None
+1 -1
View File
@@ -19,7 +19,7 @@ class Layer(Base, detachable={"features"}):
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[dict[str, Any]] = None,
renderer: Optional[Dict[str, Any]] = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
+9 -9
View File
@@ -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
+1 -1
View File
@@ -23,7 +23,7 @@ def host():
def seed_user(host):
seed = uuid.uuid4().hex
user_dict = {
"email": f"{seed[0:7]}@spockle.com",
"email": f"{seed[0:7]}@example.org",
"password": "$uper$3cr3tP@ss",
"name": f"{seed[0:7]} Name",
"company": "test spockle",
@@ -12,12 +12,13 @@ from speckle_automate import (
AutomationStatus,
run_function,
)
from speckle_automate.helpers import crypto_random_string, register_new_automation
from speckle_automate.fixtures import (
create_test_automation_run_data,
crypto_random_string,
)
from speckle_automate.schema import AutomateBase
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
@pytest.fixture
@@ -40,58 +41,16 @@ def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
return test_client
@pytest.fixture
def test_object() -> Base:
"""Create a Base model for testing."""
root_object = Base()
root_object.foo = "bar"
return root_object
@pytest.fixture
def automation_run_data(
test_object: Base, test_client: SpeckleClient, speckle_server_url: str
test_client: SpeckleClient, speckle_server_url: str
) -> AutomationRunData:
"""Set up an automation context for testing."""
project_id = test_client.stream.create("Automate function e2e test")
branch_name = "main"
"""TODO: Set up a test automation for integration testing"""
project_id = crypto_random_string(10)
test_automation_id = crypto_random_string(10)
model = test_client.branch.get(project_id, branch_name, commits_limit=1)
model_id: str = model.id
root_obj_id = operations.send(
test_object, [ServerTransport(project_id, test_client)]
)
version_id = test_client.commit.create(project_id, root_obj_id)
automation_name = crypto_random_string(10)
automation_id = crypto_random_string(10)
automation_revision_id = crypto_random_string(10)
register_new_automation(
test_client,
project_id,
model_id,
automation_id,
automation_name,
automation_revision_id,
)
automation_run_id = crypto_random_string(10)
function_id = crypto_random_string(10)
function_name = f"automate test {crypto_random_string(3)}"
return AutomationRunData(
project_id=project_id,
model_id=model_id,
branch_name=branch_name,
version_id=version_id,
speckle_server_url=speckle_server_url,
automation_id=automation_id,
automation_revision_id=automation_revision_id,
automation_run_id=automation_run_id,
function_id=function_id,
function_name=function_name,
function_logo=None,
return create_test_automation_run_data(
test_client, speckle_server_url, project_id, test_automation_id
)
@@ -189,6 +148,9 @@ def automate_function(
automation_context.mark_run_success("No forbidden types found.")
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_function_run(automation_context: AutomationContext) -> None:
"""Run an integration test for the automate function."""
automation_context = run_function(
@@ -215,6 +177,9 @@ def test_file_path():
os.remove(path)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_file_uploads(
automation_run_data: AutomationRunData, speckle_token: str, test_file_path: Path
):
@@ -230,6 +195,9 @@ def test_file_uploads(
assert len(automation_context._automation_result.blobs) == 1
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project_raises_error_for_same_model(
automation_context: AutomationContext,
) -> None:
@@ -239,6 +207,9 @@ def test_create_version_in_project_raises_error_for_same_model(
)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project(
automation_context: AutomationContext,
) -> None:
@@ -252,6 +223,9 @@ def test_create_version_in_project(
assert version_id is not None
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_set_context_view(automation_context: AutomationContext) -> None:
automation_context.set_context_view()
+2 -2
View File
@@ -183,7 +183,7 @@ class TestStream:
# NOTE: only works for server admins
# invited = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
# emails=["userA@example.org", "userB@example.org"],
# user_ids=[second_user.id],
# message="yeehaw 🤠",
# )
@@ -192,7 +192,7 @@ class TestStream:
# invited_only_email = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userC@speckle.xyz"],
# emails=["userC@example.org"],
# message="yeehaw 🤠",
# )
+10 -4
View File
@@ -100,16 +100,20 @@ def test_parse_globals_as_commit():
#! NOTE: the following three tests may not pass locally
# if you have a `speckle.xyz` account in manager
# if you have a `app.speckle.systems` account in manager
def test_get_client_without_auth():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert client is not None
def test_get_new_client_with_token(user_path):
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
client = wrap.get_client(token="super-secret-token")
@@ -117,7 +121,9 @@ def test_get_new_client_with_token(user_path):
def test_get_transport_with_token():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
wrap = StreamWrapper(
"https://app.speckle.systems/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert not client.account.token # unauthenticated bc no local accounts
@@ -0,0 +1,77 @@
import os
import uuid
from typing import List, Optional, Tuple
from urllib.parse import urlparse
import pytest
from specklepy.core.api.credentials import Account, UserInfo, get_accounts_for_server
from specklepy.core.api.models import ServerInfo, ServerMigration
from specklepy.core.helpers import speckle_path_provider
def _create_account(
id: str, url: str, movedFrom: Optional[str], movedTo: Optional[str]
) -> Account:
return Account(
id=uuid.uuid4().hex[:6].lower(),
token="myToken",
serverInfo=ServerInfo(
url=url,
name="myServer",
migration=ServerMigration(movedTo=movedTo, movedFrom=movedFrom),
),
userInfo=UserInfo(id=id),
)
def _test_cases() -> List[Tuple[List[Account], str, List[Account]]]:
user_id_1 = uuid.uuid4().hex[:6].lower()
user_id_2 = uuid.uuid4().hex[:6].lower()
old = _create_account(
user_id_1, "https://old.example.com", None, "https://new.example.com"
)
new = _create_account(
user_id_1, "https://new.example.com", "https://old.example.com", None
)
other = _create_account(user_id_2, "https://other.example.com", None, None)
given_accounts = [old, new, other]
reversed = [other, new, old]
return [
(given_accounts, "https://old.example.com", [new]),
(given_accounts, "https://new.example.com", [new]),
(reversed, "https://old.example.com", [new]),
]
def _clean_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
# deleting acc json file in json_accounts path
os.remove(os.path.join(json_accounts, f"{acc.id}.json"))
pass
def _add_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
data = Account.model_dump_json(acc)
with open(os.path.join(json_accounts, f"{acc.id}.json"), "w") as f:
f.write(data)
@pytest.mark.parametrize("accounts, requested_url, expected", _test_cases())
def test_server_migration(
accounts: List[Account], requested_url: str, expected: List[Account]
) -> None:
_add_accounts(accounts)
try:
res = get_accounts_for_server(urlparse(requested_url).netloc)
assert res == expected
finally:
_clean_accounts(accounts)