Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 24019e99f3 | |||
| 64492fafa5 | |||
| 3a8d634989 | |||
| f27650af3a | |||
| 6469b6f757 | |||
| b28db0881c | |||
| b0b442de23 | |||
| 32d2fe8ead | |||
| 9fd40eac23 | |||
| b22ba1f1f1 | |||
| 5e20fe7bf1 | |||
| 6da5da23c4 | |||
| 1b59f0b026 | |||
| 78123936d2 | |||
| dbc1aefed3 | |||
| e726345b0c | |||
| e074dbcced | |||
| 62e342b2cb | |||
| 804dd37639 | |||
| 64b61f54f5 | |||
| 58789ab234 | |||
| 2696fb74ba | |||
| 57e176af91 | |||
| 437483641c | |||
| 1e971b57c3 | |||
| f04be12ec8 | |||
| 51242928ca | |||
| 77b3be9145 | |||
| cc5abdf9cb | |||
| 4eca5144a8 | |||
| 8589663049 | |||
| 956f72dd6a | |||
| a2daa68c1c | |||
| d60feb73a2 | |||
| a0ca10ad20 | |||
| f6118f3336 | |||
| c7cd2f3e91 | |||
| b374bfefd0 | |||
| d716db251f | |||
| 6d7e7c5c4b | |||
| 7dcd9288ca | |||
| 7d99f48def | |||
| 4332a8faef | |||
| a1aee8b3fa | |||
| deb8ad50c5 | |||
| 558b25b1d1 | |||
| 4db0fa69fa | |||
| 1eca211c96 | |||
| f65173581a | |||
| 223c776c63 | |||
| ccccc53f59 | |||
| 541e3d961f | |||
| b02f183533 | |||
| 589198f5f1 | |||
| 6a9f4bf89b |
@@ -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
|
||||
|
||||
@@ -6,11 +6,11 @@ repos:
|
||||
|
||||
- repo: https://github.com/commitizen-tools/commitizen
|
||||
hooks:
|
||||
- id: commitizen
|
||||
- id: commitizen-branch
|
||||
stages:
|
||||
- push
|
||||
rev: 3.12.0
|
||||
- id: commitizen
|
||||
- id: commitizen-branch
|
||||
stages:
|
||||
- push
|
||||
rev: v3.13.0
|
||||
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.12.0
|
||||
|
||||
@@ -33,7 +33,7 @@ What is Speckle? Check our ](https://speckle.xyz) ⇒ creating an account at our public server
|
||||
- [](https://app.speckle.systems) ⇒ creating an account at our public server
|
||||
- [](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
|
||||
|
||||
### Resources
|
||||
|
||||
Generated
+848
-817
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "specklepy"
|
||||
version = "2.17.8"
|
||||
version = "2.17.14"
|
||||
description = "The Python SDK for Speckle 2.0"
|
||||
readme = "README.md"
|
||||
authors = ["Speckle Systems <devops@speckle.systems>"]
|
||||
@@ -16,9 +16,9 @@ packages = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8.0, <4.0"
|
||||
pydantic = "^2.0"
|
||||
pydantic = "^2.5"
|
||||
appdirs = "^1.4.4"
|
||||
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
|
||||
gql = { extras = ["requests", "websockets"], version = "^3.3.0" }
|
||||
ujson = "^5.3.0"
|
||||
Deprecated = "^1.2.13"
|
||||
stringcase = "^1.2.0"
|
||||
@@ -26,7 +26,7 @@ attrs = "^23.1.0"
|
||||
httpx = "^0.25.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^22.8.0"
|
||||
black = "23.11.0"
|
||||
isort = "^5.7.0"
|
||||
pytest = "^7.1.3"
|
||||
pytest-ordering = "^0.6"
|
||||
@@ -36,7 +36,7 @@ pylint = "^2.14.4"
|
||||
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,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,8 +18,9 @@ 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 import Base
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.transports.memory import MemoryTransport
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,14 +4,16 @@ Provides mechanisms to execute any function,
|
||||
that conforms to the AutomateFunction "interface"
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, TypeVar, Union, overload
|
||||
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
|
||||
|
||||
from pydantic import create_model
|
||||
from pydantic.json_schema import GenerateJsonSchema
|
||||
|
||||
from speckle_automate.automation_context import AutomationContext
|
||||
from speckle_automate.schema import AutomateBase, AutomationStatus
|
||||
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
|
||||
|
||||
T = TypeVar("T", bound=AutomateBase)
|
||||
|
||||
@@ -19,6 +21,41 @@ AutomateFunction = Callable[[AutomationContext, T], None]
|
||||
AutomateFunctionWithoutInputs = Callable[[AutomationContext], None]
|
||||
|
||||
|
||||
def _read_input_data(inputs_location: str) -> str:
|
||||
input_path = Path(inputs_location)
|
||||
if not input_path.exists():
|
||||
raise ValueError(f"Cannot find the function inputs file at {input_path}")
|
||||
|
||||
return input_path.read_text()
|
||||
|
||||
|
||||
def _parse_input_data(
|
||||
input_location: str, input_schema: Optional[type[T]]
|
||||
) -> Tuple[AutomationRunData, Optional[T], str]:
|
||||
input_json_string = _read_input_data(input_location)
|
||||
|
||||
class FunctionRunData(AutomateBase):
|
||||
speckle_token: str
|
||||
automation_run_data: AutomationRunData
|
||||
function_inputs: None = None
|
||||
|
||||
parser_model = FunctionRunData
|
||||
|
||||
if input_schema:
|
||||
parser_model = create_model(
|
||||
"FunctionRunDataWithInputs",
|
||||
function_inputs=(input_schema, ...),
|
||||
__base__=FunctionRunData,
|
||||
)
|
||||
|
||||
input_data = parser_model.model_validate_json(input_json_string)
|
||||
return (
|
||||
input_data.automation_run_data,
|
||||
input_data.function_inputs,
|
||||
input_data.speckle_token,
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def execute_automate_function(
|
||||
automate_function: AutomateFunction[T],
|
||||
@@ -32,6 +69,13 @@ def execute_automate_function(automate_function: AutomateFunctionWithoutInputs)
|
||||
...
|
||||
|
||||
|
||||
class AutomateGenerateJsonSchema(GenerateJsonSchema):
|
||||
def generate(self, schema, mode="validation"):
|
||||
json_schema = super().generate(schema, mode=mode)
|
||||
json_schema["$schema"] = self.schema_dialect
|
||||
return json_schema
|
||||
|
||||
|
||||
def execute_automate_function(
|
||||
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
|
||||
input_schema: Optional[type[T]] = None,
|
||||
@@ -40,49 +84,44 @@ def execute_automate_function(
|
||||
# first arg is the python file name, we do not need that
|
||||
args = sys.argv[1:]
|
||||
|
||||
if len(args) < 2:
|
||||
raise ValueError("too few arguments specified need minimum 2")
|
||||
|
||||
if len(args) > 4:
|
||||
raise ValueError("too many arguments specified, max supported is 4")
|
||||
if len(args) != 2:
|
||||
raise ValueError("Incorrect number of arguments specified need 2")
|
||||
|
||||
# we rely on a command name convention to decide what to do.
|
||||
# this is here, so that the function authors do not see any of this
|
||||
command = args[0]
|
||||
command, argument = args
|
||||
|
||||
if command == "generate_schema":
|
||||
path = Path(args[1])
|
||||
path = Path(argument)
|
||||
schema = json.dumps(
|
||||
input_schema.model_json_schema(by_alias=True) if input_schema else {}
|
||||
input_schema.model_json_schema(
|
||||
by_alias=True, schema_generator=AutomateGenerateJsonSchema
|
||||
)
|
||||
if input_schema
|
||||
else {}
|
||||
)
|
||||
path.write_text(schema)
|
||||
|
||||
elif command == "run":
|
||||
automation_run_data = args[1]
|
||||
function_inputs = args[2]
|
||||
automation_run_data, function_inputs, speckle_token = _parse_input_data(
|
||||
argument, input_schema
|
||||
)
|
||||
|
||||
speckle_token = os.environ.get("SPECKLE_TOKEN", None)
|
||||
if not speckle_token and len(args) != 4:
|
||||
raise ValueError("Cannot get speckle token from arguments or environment")
|
||||
|
||||
speckle_token = speckle_token if speckle_token else args[3]
|
||||
automation_context = AutomationContext.initialize(
|
||||
automation_run_data, speckle_token
|
||||
)
|
||||
|
||||
inputs = (
|
||||
input_schema.model_validate_json(function_inputs)
|
||||
if input_schema
|
||||
else input_schema
|
||||
)
|
||||
|
||||
if inputs:
|
||||
if function_inputs:
|
||||
automation_context = run_function(
|
||||
automation_context,
|
||||
automate_function, # type: ignore
|
||||
inputs,
|
||||
function_inputs, # type: ignore
|
||||
)
|
||||
|
||||
else:
|
||||
automation_context = AutomationContext.initialize(
|
||||
automation_run_data, speckle_token
|
||||
)
|
||||
automation_context = run_function(
|
||||
automation_context,
|
||||
automate_function, # type: ignore
|
||||
|
||||
@@ -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,30 @@ 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=()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from specklepy import objects
|
||||
|
||||
__all__ = ["objects"]
|
||||
|
||||
@@ -50,10 +50,16 @@ class SpeckleClient(CoreSpeckleClient):
|
||||
DEFAULT_HOST = "speckle.xyz"
|
||||
USE_SSL = True
|
||||
|
||||
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
host: str = DEFAULT_HOST,
|
||||
use_ssl: bool = USE_SSL,
|
||||
verify_certificate: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
host=host,
|
||||
use_ssl=use_ssl,
|
||||
verify_certificate=verify_certificate,
|
||||
)
|
||||
self.account = Account()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -58,13 +58,12 @@ class SpeckleClient:
|
||||
|
||||
DEFAULT_HOST = "speckle.xyz"
|
||||
USE_SSL = True
|
||||
VERIFY_CERTIFICATE = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = DEFAULT_HOST,
|
||||
use_ssl: bool = USE_SSL,
|
||||
verify_certificate: bool = VERIFY_CERTIFICATE,
|
||||
verify_certificate: bool = True,
|
||||
) -> None:
|
||||
ws_protocol = "ws"
|
||||
http_protocol = "http"
|
||||
@@ -124,8 +123,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:
|
||||
"""
|
||||
@@ -136,7 +134,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -196,3 +201,5 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@ from gql import gql
|
||||
|
||||
from specklepy.core.api.models import Branch
|
||||
from specklepy.core.api.resource import ResourceBase
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
|
||||
NAME = "branch"
|
||||
|
||||
@@ -39,6 +40,8 @@ class Resource(ResourceBase):
|
||||
}
|
||||
"""
|
||||
)
|
||||
if len(name) < 3:
|
||||
return SpeckleException(message="Branch Name must be at least 3 characters")
|
||||
params = {
|
||||
"branch": {
|
||||
"streamId": stream_id,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import requests
|
||||
from gql import gql
|
||||
|
||||
from specklepy.core.api.models import ServerInfo
|
||||
@@ -56,9 +57,21 @@ class Resource(ResourceBase):
|
||||
"""
|
||||
)
|
||||
|
||||
return self.make_request(
|
||||
server_info = self.make_request(
|
||||
query=query, return_type="serverInfo", schema=ServerInfo
|
||||
)
|
||||
if isinstance(server_info, ServerInfo) and isinstance(
|
||||
server_info.canonicalUrl, str
|
||||
):
|
||||
r = requests.get(
|
||||
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
|
||||
)
|
||||
if "x-speckle-frontend-2" in r.headers:
|
||||
server_info.frontend2 = True
|
||||
else:
|
||||
server_info.frontend2 = False
|
||||
|
||||
return server_info
|
||||
|
||||
def version(self) -> Tuple[Any, ...]:
|
||||
"""Get the server version
|
||||
|
||||
@@ -166,7 +166,8 @@ 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}
|
||||
}
|
||||
@@ -730,13 +731,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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from urllib.parse import unquote, urlparse
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
from warnings import warn
|
||||
|
||||
from gql import gql
|
||||
@@ -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
|
||||
@@ -47,6 +47,7 @@ class StreamWrapper:
|
||||
commit_id: str = None
|
||||
object_id: str = None
|
||||
branch_name: str = None
|
||||
model_id: str = None
|
||||
_client: SpeckleClient = None
|
||||
_account: Account = None
|
||||
|
||||
@@ -86,45 +87,58 @@ class StreamWrapper:
|
||||
# check for fe2 URL
|
||||
if "/projects/" in parsed.path:
|
||||
use_fe2 = True
|
||||
key_stream = "project"
|
||||
else:
|
||||
use_fe2 = False
|
||||
key_stream = "stream"
|
||||
|
||||
while segments:
|
||||
segment = segments.pop(0)
|
||||
if segments and (
|
||||
(use_fe2 is False and segment.lower() == "streams")
|
||||
or (use_fe2 is True and segment.lower() == "projects")
|
||||
):
|
||||
self.stream_id = segments.pop(0)
|
||||
elif segments and segment.lower() == "commits":
|
||||
self.commit_id = segments.pop(0)
|
||||
elif segments and (
|
||||
(use_fe2 is False and segment.lower() == "branches")
|
||||
or (use_fe2 is True and segment.lower() == "models")
|
||||
):
|
||||
self.branch_name = unquote(segments.pop(0))
|
||||
elif segments and segment.lower() == "objects":
|
||||
self.object_id = segments.pop(0)
|
||||
elif segment.lower() == "globals":
|
||||
self.branch_name = "globals"
|
||||
if segments:
|
||||
|
||||
if use_fe2 is False:
|
||||
if segments and segment.lower() == "streams":
|
||||
self.stream_id = segments.pop(0)
|
||||
elif segments and segment.lower() == "commits":
|
||||
self.commit_id = segments.pop(0)
|
||||
else:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - invalid URL"
|
||||
" provided."
|
||||
)
|
||||
elif segments and segment.lower() == "branches":
|
||||
self.branch_name = unquote(segments.pop(0))
|
||||
elif segments and segment.lower() == "objects":
|
||||
self.object_id = segments.pop(0)
|
||||
elif segment.lower() == "globals":
|
||||
self.branch_name = "globals"
|
||||
if segments:
|
||||
self.commit_id = segments.pop(0)
|
||||
else:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - invalid URL"
|
||||
" provided."
|
||||
)
|
||||
elif segments and use_fe2 is True:
|
||||
if segment.lower() == "projects":
|
||||
self.stream_id = segments.pop(0)
|
||||
elif segment.lower() == "models":
|
||||
next_segment = segments.pop(0)
|
||||
if "," in next_segment:
|
||||
raise SpeckleException("Multi-model urls are not supported yet")
|
||||
elif unquote(next_segment).startswith("$"):
|
||||
raise SpeckleException(
|
||||
"Federation model urls are not supported"
|
||||
)
|
||||
elif len(next_segment) == 32:
|
||||
self.object_id = next_segment
|
||||
else:
|
||||
self.branch_name = unquote(next_segment).split("@")[0]
|
||||
if "@" in unquote(next_segment):
|
||||
self.commit_id = unquote(next_segment).split("@")[1]
|
||||
|
||||
else:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - invalid URL"
|
||||
" provided."
|
||||
)
|
||||
|
||||
if use_fe2 is True and self.branch_name is not None:
|
||||
if "," in self.branch_name:
|
||||
raise SpeckleException("Multi-model urls are not supported yet")
|
||||
|
||||
if "@" in self.branch_name:
|
||||
model_id = self.branch_name.split("@")[0]
|
||||
self.commit_id = self.branch_name.split("@")[1]
|
||||
else:
|
||||
model_id = self.branch_name
|
||||
|
||||
self.model_id = self.branch_name
|
||||
# get branch name
|
||||
query = gql(
|
||||
"""
|
||||
@@ -139,7 +153,7 @@ class StreamWrapper:
|
||||
"""
|
||||
)
|
||||
self._client = self.get_client()
|
||||
params = {"project_id": self.stream_id, "model_id": model_id}
|
||||
params = {"project_id": self.stream_id, "model_id": self.model_id}
|
||||
project = self._client.httpclient.execute(query, params)
|
||||
|
||||
try:
|
||||
@@ -149,7 +163,7 @@ class StreamWrapper:
|
||||
|
||||
if not self.stream_id:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - no stream id found."
|
||||
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -164,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)
|
||||
@@ -230,3 +237,46 @@ class StreamWrapper:
|
||||
if not self._account or not self._account.token:
|
||||
self.get_account(token)
|
||||
return ServerTransport(self.stream_id, account=self._account)
|
||||
|
||||
def to_string(self) -> str:
|
||||
"""
|
||||
Constructs a URL depending on the StreamWrapper type and FE version.
|
||||
"""
|
||||
use_fe2 = False
|
||||
key_streams = "/streams/"
|
||||
key_branches = "/branches/"
|
||||
if isinstance(self.branch_name, str):
|
||||
value_branch = quote(self.branch_name)
|
||||
if self.branch_name == "globals":
|
||||
key_branches = "/"
|
||||
key_commits = "/commits/"
|
||||
if isinstance(self.commit_id, str) and self.branch_name == "globals":
|
||||
key_commits = "/globals/"
|
||||
key_objects = "/objects/"
|
||||
|
||||
if "/projects/" in self.stream_url:
|
||||
use_fe2 = True
|
||||
key_streams = "/projects/"
|
||||
key_branches = "/models/"
|
||||
value_branch = self.model_id
|
||||
key_commits = "@"
|
||||
key_objects = "/models/"
|
||||
|
||||
wrapper_type = self.type
|
||||
if use_fe2 is False or (use_fe2 is True and not self.model_id):
|
||||
base_url = f"{self.server_url}{key_streams}{self.stream_id}"
|
||||
else: # fe2 is True and model_id available
|
||||
base_url = f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
|
||||
|
||||
if wrapper_type == "object":
|
||||
return f"{base_url}{key_objects}{self.object_id}"
|
||||
elif wrapper_type == "commit":
|
||||
return f"{base_url}{key_commits}{self.commit_id}"
|
||||
elif wrapper_type == "branch":
|
||||
return f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
|
||||
elif wrapper_type == "stream":
|
||||
return f"{self.server_url}{key_streams}{self.stream_id}"
|
||||
else:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse StreamWrapper of type '{wrapper_type}'"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
class CRS(Base, speckle_type="Objects.GIS.CRS"):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import (
|
||||
Arc,
|
||||
Circle,
|
||||
|
||||
@@ -13,13 +13,13 @@ class Layer(Base, detachable={"features"}):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str = None,
|
||||
crs: CRS = None,
|
||||
name: Optional[str] = None,
|
||||
crs: Optional[CRS] = None,
|
||||
units: str = "m",
|
||||
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)
|
||||
@@ -39,7 +39,6 @@ class VectorLayer(
|
||||
speckle_type="VectorLayer",
|
||||
serialize_ignore={"features"},
|
||||
):
|
||||
|
||||
"""GIS Vector Layer"""
|
||||
|
||||
name: Optional[str] = None
|
||||
@@ -68,7 +67,6 @@ class RasterLayer(
|
||||
speckle_type="RasterLayer",
|
||||
serialize_ignore={"features"},
|
||||
):
|
||||
|
||||
"""GIS Raster Layer"""
|
||||
|
||||
name: Optional[str] = None
|
||||
@@ -96,7 +94,6 @@ class VectorLayer( # noqa: F811
|
||||
speckle_type="Objects.GIS.VectorLayer",
|
||||
serialize_ignore={"features"},
|
||||
):
|
||||
|
||||
"""GIS Vector Layer"""
|
||||
|
||||
name: Optional[str] = None
|
||||
@@ -124,7 +121,6 @@ class RasterLayer( # noqa: F811
|
||||
speckle_type="Objects.GIS.RasterLayer",
|
||||
serialize_ignore={"features"},
|
||||
):
|
||||
|
||||
"""GIS Raster Layer"""
|
||||
|
||||
name: Optional[str] = None
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
"""Builtin Speckle object kit."""
|
||||
|
||||
from specklepy.objects import encoding, geometry, other, primitive, structural, units
|
||||
from specklepy.objects import (
|
||||
GIS,
|
||||
encoding,
|
||||
geometry,
|
||||
other,
|
||||
primitive,
|
||||
structural,
|
||||
units,
|
||||
)
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
|
||||
__all__ = [
|
||||
"Base",
|
||||
"encoding",
|
||||
"geometry",
|
||||
"other",
|
||||
"units",
|
||||
"structural",
|
||||
"primitive",
|
||||
"GIS",
|
||||
]
|
||||
|
||||
@@ -188,7 +188,8 @@ class _RegisteringBase:
|
||||
cls._detachable = cls._detachable.union(detachable)
|
||||
if serialize_ignore:
|
||||
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
|
||||
super().__init_subclass__(**kwargs)
|
||||
# we know, that the super here is object, that takes no args on init subclass
|
||||
return super().__init_subclass__()
|
||||
|
||||
|
||||
# T = TypeVar("T")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 import Base
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
class ITraversalRule(Protocol):
|
||||
|
||||
@@ -73,7 +73,7 @@ class ServerTransport(AbstractTransport):
|
||||
warn(
|
||||
SpeckleWarning(
|
||||
"Unauthenticated Speckle Client provided to Server Transport"
|
||||
f" for {self.url}. Receiving from private streams will fail."
|
||||
f" for {url}. Receiving from private streams will fail."
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -84,14 +84,18 @@ class ServerTransport(AbstractTransport):
|
||||
self.stream_id = stream_id
|
||||
self.url = url
|
||||
|
||||
self._batch_sender = BatchSender(
|
||||
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
|
||||
)
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update(
|
||||
{"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
|
||||
)
|
||||
|
||||
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",
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
+15
@@ -189,6 +189,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 +218,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 +236,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 +248,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 +264,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()
|
||||
|
||||
@@ -18,6 +18,7 @@ class TestServer:
|
||||
server = client.server.get()
|
||||
|
||||
assert isinstance(server, ServerInfo)
|
||||
assert isinstance(server.frontend2, bool)
|
||||
|
||||
def test_server_version(self, client: SpeckleClient):
|
||||
version = client.server.version()
|
||||
@@ -44,6 +44,14 @@ 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)
|
||||
|
||||
@@ -2,11 +2,13 @@ import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from urllib.parse import unquote
|
||||
|
||||
import pytest
|
||||
|
||||
from specklepy.api.wrapper import StreamWrapper
|
||||
from specklepy.core.helpers import speckle_path_provider
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
@@ -29,6 +31,22 @@ def user_path() -> Iterable[Path]:
|
||||
speckle_path_provider.override_application_data_path(None)
|
||||
|
||||
|
||||
def test_parse_empty():
|
||||
try:
|
||||
StreamWrapper("https://testing.speckle.dev/streams")
|
||||
assert False
|
||||
except SpeckleException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_parse_empty_fe2():
|
||||
try:
|
||||
StreamWrapper("https://latest.speckle.systems/projects")
|
||||
assert False
|
||||
except SpeckleException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_parse_stream():
|
||||
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
|
||||
assert wrap.type == "stream"
|
||||
@@ -142,8 +160,56 @@ def test_parse_model():
|
||||
assert wrap.type == "branch"
|
||||
|
||||
|
||||
def test_parse_federated_model():
|
||||
try:
|
||||
StreamWrapper("https://latest.speckle.systems/projects/843d07eb10/models/$main")
|
||||
assert False
|
||||
except SpeckleException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_parse_multi_model():
|
||||
try:
|
||||
StreamWrapper(
|
||||
"https://latest.speckle.systems/projects/2099ac4b5f/models/1870f279e3,a9cfdddc79"
|
||||
)
|
||||
assert False
|
||||
except SpeckleException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_parse_object_fe2():
|
||||
wrap = StreamWrapper(
|
||||
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c"
|
||||
)
|
||||
assert wrap.type == "object"
|
||||
|
||||
|
||||
def test_parse_version():
|
||||
wrap = StreamWrapper(
|
||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1"
|
||||
)
|
||||
wrap_quoted = StreamWrapper(
|
||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1"
|
||||
)
|
||||
assert wrap.type == "commit"
|
||||
assert wrap_quoted.type == "commit"
|
||||
|
||||
|
||||
def test_to_string():
|
||||
urls = [
|
||||
"https://testing.speckle.dev/streams/a75ab4f10f",
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F",
|
||||
"https://testing.speckle.dev/streams/0c6ad366c4/globals",
|
||||
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893",
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792",
|
||||
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6",
|
||||
"https://latest.speckle.systems/projects/843d07eb10",
|
||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838",
|
||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1",
|
||||
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1",
|
||||
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c",
|
||||
]
|
||||
for url in urls:
|
||||
wrap = StreamWrapper(url)
|
||||
assert unquote(wrap.to_string()) == unquote(url)
|
||||
@@ -0,0 +1,77 @@
|
||||
import os
|
||||
import uuid
|
||||
from typing import List, Optional, Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
from specklepy.core.api.credentials import Account, UserInfo, get_accounts_for_server
|
||||
from specklepy.core.api.models import ServerInfo, ServerMigration
|
||||
from specklepy.core.helpers import speckle_path_provider
|
||||
|
||||
|
||||
def _create_account(
|
||||
id: str, url: str, movedFrom: Optional[str], movedTo: Optional[str]
|
||||
) -> Account:
|
||||
return Account(
|
||||
id=uuid.uuid4().hex[:6].lower(),
|
||||
token="myToken",
|
||||
serverInfo=ServerInfo(
|
||||
url=url,
|
||||
name="myServer",
|
||||
migration=ServerMigration(movedTo=movedTo, movedFrom=movedFrom),
|
||||
),
|
||||
userInfo=UserInfo(id=id),
|
||||
)
|
||||
|
||||
|
||||
def _test_cases() -> List[Tuple[List[Account], str, List[Account]]]:
|
||||
user_id_1 = uuid.uuid4().hex[:6].lower()
|
||||
user_id_2 = uuid.uuid4().hex[:6].lower()
|
||||
old = _create_account(
|
||||
user_id_1, "https://old.example.com", None, "https://new.example.com"
|
||||
)
|
||||
new = _create_account(
|
||||
user_id_1, "https://new.example.com", "https://old.example.com", None
|
||||
)
|
||||
other = _create_account(user_id_2, "https://other.example.com", None, None)
|
||||
|
||||
given_accounts = [old, new, other]
|
||||
reversed = [other, new, old]
|
||||
|
||||
return [
|
||||
(given_accounts, "https://old.example.com", [new]),
|
||||
(given_accounts, "https://new.example.com", [new]),
|
||||
(reversed, "https://old.example.com", [new]),
|
||||
]
|
||||
|
||||
|
||||
def _clean_accounts(accounts: List[Account]) -> None:
|
||||
json_accounts = speckle_path_provider.accounts_folder_path()
|
||||
|
||||
for acc in accounts:
|
||||
# deleting acc json file in json_accounts path
|
||||
os.remove(os.path.join(json_accounts, f"{acc.id}.json"))
|
||||
pass
|
||||
|
||||
|
||||
def _add_accounts(accounts: List[Account]) -> None:
|
||||
json_accounts = speckle_path_provider.accounts_folder_path()
|
||||
|
||||
for acc in accounts:
|
||||
data = Account.model_dump_json(acc)
|
||||
with open(os.path.join(json_accounts, f"{acc.id}.json"), "w") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("accounts, requested_url, expected", _test_cases())
|
||||
def test_server_migration(
|
||||
accounts: List[Account], requested_url: str, expected: List[Account]
|
||||
) -> None:
|
||||
_add_accounts(accounts)
|
||||
try:
|
||||
res = get_accounts_for_server(urlparse(requested_url).netloc)
|
||||
assert res == expected
|
||||
|
||||
finally:
|
||||
_clean_accounts(accounts)
|
||||
Reference in New Issue
Block a user