Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 825097e1a6 | |||
| d3ab26240a | |||
| ce6be1a98e | |||
| 213e73dfdd | |||
| 15129df7ce | |||
| 88519ce8b0 | |||
| d4f94450a5 | |||
| 4c46201526 | |||
| 75b064b3c7 | |||
| 1198f2e2ad | |||
| 7ab787bfb1 | |||
| bbbf373b50 | |||
| f34e4a2874 | |||
| 45ebc375ad | |||
| 4c41fa79fc | |||
| 0aa14ca077 | |||
| 6bfdf8850c |
@@ -1,27 +0,0 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/.devcontainer/base.Dockerfile
|
||||
|
||||
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
|
||||
ARG VARIANT="3.10"
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
|
||||
|
||||
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
|
||||
ARG NODE_VERSION="16"
|
||||
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
|
||||
|
||||
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
|
||||
# COPY requirements.txt /tmp/pip-tmp/
|
||||
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
|
||||
# && rm -rf /tmp/pip-tmp
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
|
||||
# [Optional] Uncomment this line to install global node packages.
|
||||
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
|
||||
|
||||
USER vscode
|
||||
|
||||
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||
|
||||
ENV PATH=$PATH:$HOME/.poetry/env
|
||||
@@ -1,55 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/python-3
|
||||
{
|
||||
"name": "Python 3",
|
||||
// "build": {
|
||||
// "dockerfile": "Dockerfile",
|
||||
// "context": "..",
|
||||
// "args": {
|
||||
// // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9
|
||||
// "VARIANT": "3.6",
|
||||
// // Options
|
||||
// "NODE_VERSION": "lts/*"
|
||||
// }
|
||||
// },
|
||||
"dockerComposeFile": "./docker-compose.yaml",
|
||||
"service": "specklepy",
|
||||
"workspaceFolder": "/workspaces/specklepy",
|
||||
"shutdownAction": "stopCompose",
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"python.pythonPath": "/usr/local/bin/python",
|
||||
"python.languageServer": "Pylance",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.linting.pylintArgs": [
|
||||
"--max-line-length=120"
|
||||
],
|
||||
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
|
||||
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
|
||||
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
|
||||
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
|
||||
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
|
||||
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
|
||||
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
|
||||
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
|
||||
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
|
||||
"python.testing.pytestArgs": [
|
||||
"tests/",
|
||||
"-s"
|
||||
],
|
||||
"python.testing.pytestEnabled": true,
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "poetry config virtualenvs.create false && poetry install",
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
version: "3.3" # optional since v1.27.0
|
||||
services:
|
||||
postgres:
|
||||
image: cimg/postgres:14.2
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
POSTGRES_USER: speckle
|
||||
network_mode: host
|
||||
redis:
|
||||
image: cimg/redis:6.2
|
||||
network_mode: host
|
||||
speckle-server:
|
||||
image: speckle/speckle-server:latest
|
||||
command: ["bash", "-c", "/wait && node bin/www"]
|
||||
environment:
|
||||
POSTGRES_URL: "localhost"
|
||||
POSTGRES_USER: "speckle"
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle2_test"
|
||||
REDIS_URL: "redis://localhost"
|
||||
SESSION_SECRET: "keyboard cat"
|
||||
STRATEGY_LOCAL: "true"
|
||||
CANONICAL_URL: "http://localhost:3000"
|
||||
WAIT_HOSTS: localhost:5432, localhost:6379
|
||||
DISABLE_FILE_UPLOADS: "true"
|
||||
network_mode: host
|
||||
|
||||
specklepy:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
args:
|
||||
VARIANT: 3.9
|
||||
NODE_VERSION: lts/*
|
||||
volumes:
|
||||
# Mounts the project folder to '/workspace'. While this file is in .devcontainer,
|
||||
# mounts are relative to the first file in the list, which is a level up.
|
||||
- ..:/workspaces/specklepy:cached
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: /bin/sh -c "while sleep 1000; do :; done"
|
||||
network_mode: host
|
||||
# networks:
|
||||
# default:
|
||||
@@ -3,12 +3,10 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "v3-dev"
|
||||
push:
|
||||
branches:
|
||||
- "v3-dev"
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: continuous-integration
|
||||
build-and-test:
|
||||
name: build-and-test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -39,18 +37,18 @@ jobs:
|
||||
- name: Run pre-commit
|
||||
run: uv run pre-commit run --all-files
|
||||
|
||||
# - name: Run Speckle Server
|
||||
# run: docker compose up -d
|
||||
- name: Run Speckle Server
|
||||
run: docker compose up --detach --wait
|
||||
|
||||
# - name: Run tests
|
||||
# run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
- name: Run tests
|
||||
run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
# - uses: codecov/codecov-action@v5
|
||||
# if: matrix.python-version == 3.13
|
||||
# with:
|
||||
# fail_ci_if_error: true # optional (default = false)
|
||||
# files: ./reports/test-results.xml # optional
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: matrix.python-version == 3.13
|
||||
with:
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
files: ./reports/test-results.xml # optional
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
@@ -1,33 +1,98 @@
|
||||
# Publish a release to PyPI.
|
||||
name: "Publish to PyPI"
|
||||
|
||||
name: "Publish Python Package"
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Specklepy test and build"]
|
||||
branches: [v3-dev]
|
||||
types:
|
||||
- completed
|
||||
push:
|
||||
branches:
|
||||
- "v3-dev"
|
||||
tags:
|
||||
- "3.*.*"
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: Upload to PyPI
|
||||
build-and-test:
|
||||
name: continuous-integration
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pre-commit/
|
||||
key: ${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
|
||||
- name: Run pre-commit
|
||||
run: uv run pre-commit run --all-files
|
||||
|
||||
- name: Run Speckle Server
|
||||
run: docker compose up -d
|
||||
|
||||
# - name: Run tests
|
||||
# run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
# - uses: codecov/codecov-action@v5
|
||||
# if: matrix.python-version == 3.13
|
||||
# with:
|
||||
# fail_ci_if_error: true # optional (default = false)
|
||||
# files: ./reports/test-results.xml # optional
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
publish-package:
|
||||
name: "Build and Publish Python Package"
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-test
|
||||
|
||||
# set the environment based on what triggered the workflow
|
||||
environment:
|
||||
name: testpypi
|
||||
name: ${{ github.ref_type == 'tag' && 'pypi' || 'testpypi' }}
|
||||
|
||||
permissions:
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# This is necessary so that we have the tags.
|
||||
fetch-depth: 0
|
||||
- name: "Build artifacts"
|
||||
|
||||
- name: "Build package artifacts"
|
||||
run: uv build
|
||||
- name: Publish to PyPi
|
||||
|
||||
# Logic for TestPyPI (on v3-dev branch push)
|
||||
- name: "Publish to TestPyPI"
|
||||
if: ${{ github.ref_type == 'branch' }}
|
||||
run: uv publish --index test
|
||||
|
||||
- name: Test package install
|
||||
- name: "Verify TestPyPI package installation"
|
||||
if: ${{ github.ref_type == 'branch' }}
|
||||
run: uv run --index test --with specklepy --no-project -- python -c "import specklepy"
|
||||
|
||||
# Logic for PyPI (on v3* tag creation)
|
||||
- name: "Publish to PyPI"
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: uv publish
|
||||
|
||||
- name: "Verify PyPI package installation"
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
run: uv run --with specklepy --no-project -- python -c "import specklepy"
|
||||
|
||||
+3
-4
@@ -13,7 +13,7 @@ services:
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- ./.volumes/postgres-data:/var/lib/postgresql/data/
|
||||
- postgres-data:/var/lib/postgresql/data/
|
||||
healthcheck:
|
||||
# the -U user has to match the POSTGRES_USER value
|
||||
test: ["CMD-SHELL", "pg_isready -U speckle"]
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
image: "redis:6.0-alpine"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/redis-data:/data
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 5s
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
command: server /data --console-address ":9001"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/minio-data:/data
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -100,7 +100,6 @@ services:
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
FRONTEND_ORIGIN: "http://127.0.0.1:8080"
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
@@ -10,6 +10,7 @@ from specklepy.api.resources import (
|
||||
ServerResource,
|
||||
SubscriptionResource,
|
||||
VersionResource,
|
||||
WorkspaceResource,
|
||||
)
|
||||
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
|
||||
from specklepy.logging import metrics
|
||||
@@ -111,6 +112,12 @@ class SpeckleClient(CoreSpeckleClient):
|
||||
client=self.httpclient,
|
||||
server_version=server_version,
|
||||
)
|
||||
self.workspace = WorkspaceResource(
|
||||
account=self.account,
|
||||
basepath=self.url,
|
||||
client=self.httpclient,
|
||||
server_version=server_version,
|
||||
)
|
||||
self.subscription = SubscriptionResource(
|
||||
account=self.account,
|
||||
basepath=self.ws_url,
|
||||
|
||||
@@ -8,6 +8,7 @@ from specklepy.api.resources.current.project_resource import ProjectResource
|
||||
from specklepy.api.resources.current.server_resource import ServerResource
|
||||
from specklepy.api.resources.current.subscription_resource import SubscriptionResource
|
||||
from specklepy.api.resources.current.version_resource import VersionResource
|
||||
from specklepy.api.resources.current.workspace_resource import WorkspaceResource
|
||||
|
||||
__all__ = [
|
||||
"ActiveUserResource",
|
||||
@@ -18,4 +19,5 @@ __all__ = [
|
||||
"ServerResource",
|
||||
"SubscriptionResource",
|
||||
"VersionResource",
|
||||
"WorkspaceResource",
|
||||
]
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
|
||||
from specklepy.core.api.inputs.user_inputs import (
|
||||
UserProjectsFilter,
|
||||
UserUpdateInput,
|
||||
UserWorkspacesFilter,
|
||||
)
|
||||
from specklepy.core.api.models import (
|
||||
PendingStreamCollaborator,
|
||||
Project,
|
||||
ResourceCollection,
|
||||
User,
|
||||
)
|
||||
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
|
||||
from specklepy.core.api.resources import ActiveUserResource as CoreResource
|
||||
from specklepy.logging import metrics
|
||||
|
||||
@@ -51,3 +56,26 @@ class ActiveUserResource(CoreResource):
|
||||
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}
|
||||
)
|
||||
return super().get_project_invites()
|
||||
|
||||
def can_create_personal_projects(self) -> PermissionCheckResult:
|
||||
metrics.track(
|
||||
metrics.SDK,
|
||||
self.account,
|
||||
{"name": "Active User Can Create Personal Projects Check"},
|
||||
)
|
||||
return super().can_create_personal_projects()
|
||||
|
||||
def get_workspaces(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
filter: Optional[UserWorkspacesFilter] = None,
|
||||
) -> ResourceCollection[Workspace]:
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Workspaces"})
|
||||
return super().get_workspaces(limit, cursor, filter)
|
||||
|
||||
def get_active_workspace(self) -> Optional[Workspace]:
|
||||
metrics.track(
|
||||
metrics.SDK, self.account, {"name": "Active User Get Active Workspace"}
|
||||
)
|
||||
return super().get_active_workspace()
|
||||
|
||||
@@ -5,8 +5,10 @@ from specklepy.core.api.inputs.project_inputs import (
|
||||
ProjectModelsFilter,
|
||||
ProjectUpdateInput,
|
||||
ProjectUpdateRoleInput,
|
||||
WorkspaceProjectCreateInput,
|
||||
)
|
||||
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
|
||||
from specklepy.core.api.models.current import ProjectPermissionChecks
|
||||
from specklepy.core.api.resources import ProjectResource as CoreResource
|
||||
from specklepy.logging import metrics
|
||||
|
||||
@@ -26,6 +28,12 @@ class ProjectResource(CoreResource):
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Project Get "})
|
||||
return super().get(project_id)
|
||||
|
||||
def get_permissions(self, project_id: str) -> ProjectPermissionChecks:
|
||||
metrics.track(
|
||||
metrics.SDK, self.account, {"name": "Project Project Permissions "}
|
||||
)
|
||||
return super().get_permissions(project_id)
|
||||
|
||||
def get_with_models(
|
||||
self,
|
||||
project_id: str,
|
||||
@@ -50,6 +58,10 @@ class ProjectResource(CoreResource):
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Project Create"})
|
||||
return super().create(input)
|
||||
|
||||
def create_in_workspace(self, input: WorkspaceProjectCreateInput) -> Project:
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Workspace Project Create"})
|
||||
return super().create_in_workspace(input)
|
||||
|
||||
def update(self, input: ProjectUpdateInput) -> Project:
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Project Update"})
|
||||
return super().update(input)
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import Optional
|
||||
|
||||
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
|
||||
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
|
||||
from specklepy.core.api.resources import WorkspaceResource as CoreResource
|
||||
from specklepy.logging import metrics
|
||||
|
||||
|
||||
class WorkspaceResource(CoreResource):
|
||||
"""API Access class for workspace"""
|
||||
|
||||
def __init__(self, account, basepath, client, server_version) -> None:
|
||||
super().__init__(
|
||||
account=account,
|
||||
basepath=basepath,
|
||||
client=client,
|
||||
server_version=server_version,
|
||||
)
|
||||
|
||||
def get(self, workspace_id: str) -> Workspace:
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Workspace Get"})
|
||||
return super().get(workspace_id)
|
||||
|
||||
def get_projects(
|
||||
self,
|
||||
workspace_id: str,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
filter: Optional[WorksaceProjectsFilter] = None,
|
||||
) -> ResourceCollection[Project]:
|
||||
metrics.track(metrics.SDK, self.account, {"name": "Workspace Get Projects"})
|
||||
return super().get_projects(workspace_id, limit, cursor, filter)
|
||||
@@ -18,6 +18,7 @@ from specklepy.core.api.resources import (
|
||||
ServerResource,
|
||||
SubscriptionResource,
|
||||
VersionResource,
|
||||
WorkspaceResource,
|
||||
)
|
||||
from specklepy.logging import metrics
|
||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||
@@ -223,6 +224,12 @@ class SpeckleClient:
|
||||
client=self.httpclient,
|
||||
server_version=server_version,
|
||||
)
|
||||
self.workspace = WorkspaceResource(
|
||||
account=self.account,
|
||||
basepath=self.url,
|
||||
client=self.httpclient,
|
||||
server_version=server_version,
|
||||
)
|
||||
self.subscription = SubscriptionResource(
|
||||
account=self.account,
|
||||
basepath=self.ws_url,
|
||||
|
||||
@@ -10,6 +10,13 @@ class ProjectCreateInput(GraphQLBaseModel):
|
||||
visibility: Optional[ProjectVisibility]
|
||||
|
||||
|
||||
class WorkspaceProjectCreateInput(GraphQLBaseModel):
|
||||
name: Optional[str]
|
||||
description: Optional[str]
|
||||
visibility: Optional[ProjectVisibility]
|
||||
workspaceId: str
|
||||
|
||||
|
||||
class ProjectInviteCreateInput(GraphQLBaseModel):
|
||||
email: Optional[str]
|
||||
role: Optional[str]
|
||||
@@ -44,3 +51,12 @@ class ProjectUpdateRoleInput(GraphQLBaseModel):
|
||||
user_id: str
|
||||
project_id: str
|
||||
role: Optional[str]
|
||||
|
||||
|
||||
class WorksaceProjectsFilter(GraphQLBaseModel):
|
||||
search: Optional[str]
|
||||
"""Filter out projects by name"""
|
||||
with_project_role_only: Optional[bool]
|
||||
"""
|
||||
Only return workspace projects that the active user has an explicit project role in
|
||||
"""
|
||||
|
||||
@@ -11,5 +11,12 @@ class UserUpdateInput(GraphQLBaseModel):
|
||||
|
||||
|
||||
class UserProjectsFilter(GraphQLBaseModel):
|
||||
search: str
|
||||
search: Optional[str] = None
|
||||
only_with_roles: Optional[Sequence[str]] = None
|
||||
workspace_id: Optional[str] = None
|
||||
personal_only: Optional[bool] = None
|
||||
include_implicit_access: Optional[bool] = None
|
||||
|
||||
|
||||
class UserWorkspacesFilter(GraphQLBaseModel):
|
||||
search: Optional[str]
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Generic, List, Optional, TypeVar
|
||||
|
||||
from specklepy.core.api.enums import ProjectVisibility
|
||||
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
|
||||
from specklepy.logging.exceptions import WorkspacePermissionException
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
@@ -52,6 +53,10 @@ class ServerConfiguration(GraphQLBaseModel):
|
||||
object_size_limit_bytes: int
|
||||
|
||||
|
||||
class ServerWorkspacesInfo(GraphQLBaseModel):
|
||||
workspaces_enabled: bool
|
||||
|
||||
|
||||
# Keeping this one all Optionals at the minute,
|
||||
# because its used both as a deserialization model for GQL and Account Management
|
||||
class ServerInfo(GraphQLBaseModel):
|
||||
@@ -61,13 +66,11 @@ class ServerInfo(GraphQLBaseModel):
|
||||
admin_contact: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
canonical_url: Optional[str] = None
|
||||
roles: Optional[List[dict]] = None
|
||||
scopes: Optional[List[dict]] = None
|
||||
auth_strategies: Optional[List[dict]] = None
|
||||
version: Optional[str] = None
|
||||
frontend2: Optional[bool] = None
|
||||
migration: Optional[ServerMigration] = None
|
||||
|
||||
workspaces: Optional[ServerWorkspacesInfo] = None
|
||||
# TODO separate gql model from account management model
|
||||
|
||||
|
||||
@@ -129,7 +132,8 @@ class Version(GraphQLBaseModel):
|
||||
id: str
|
||||
message: Optional[str]
|
||||
preview_url: str
|
||||
referenced_object: str
|
||||
referenced_object: Optional[str]
|
||||
"""Maybe null if workspaces version history limit has been exceeded"""
|
||||
source_application: Optional[str]
|
||||
|
||||
|
||||
@@ -148,6 +152,11 @@ class ModelWithVersions(Model):
|
||||
versions: ResourceCollection[Version]
|
||||
|
||||
|
||||
class ProjectPermissionChecks(GraphQLBaseModel):
|
||||
can_create_model: "PermissionCheckResult"
|
||||
can_delete: "PermissionCheckResult"
|
||||
|
||||
|
||||
class Project(GraphQLBaseModel):
|
||||
allow_public_comments: bool
|
||||
created_at: datetime
|
||||
@@ -177,3 +186,36 @@ class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
|
||||
class UserSearchResultCollection(GraphQLBaseModel):
|
||||
items: List[LimitedUser]
|
||||
cursor: Optional[str] = None
|
||||
|
||||
|
||||
class PermissionCheckResult(GraphQLBaseModel):
|
||||
authorized: bool
|
||||
code: str
|
||||
message: str
|
||||
|
||||
def ensure_authorised(self) -> None:
|
||||
"""Raises WorkspacePermissionException if not authorized"""
|
||||
if not self.authorized:
|
||||
raise WorkspacePermissionException(self.message)
|
||||
|
||||
|
||||
class WorkspacePermissionChecks(GraphQLBaseModel):
|
||||
can_create_project: PermissionCheckResult
|
||||
|
||||
|
||||
class WorkspaceCreationState(GraphQLBaseModel):
|
||||
completed: bool
|
||||
|
||||
|
||||
class Workspace(GraphQLBaseModel):
|
||||
id: str
|
||||
name: str
|
||||
role: Optional[str]
|
||||
slug: str
|
||||
logo: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
read_only: bool
|
||||
description: Optional[str]
|
||||
creation_state: Optional[WorkspaceCreationState]
|
||||
permissions: WorkspacePermissionChecks
|
||||
|
||||
@@ -10,6 +10,7 @@ from specklepy.core.api.resources.current.subscription_resource import (
|
||||
SubscriptionResource,
|
||||
)
|
||||
from specklepy.core.api.resources.current.version_resource import VersionResource
|
||||
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
|
||||
|
||||
__all__ = [
|
||||
"ActiveUserResource",
|
||||
@@ -20,4 +21,5 @@ __all__ = [
|
||||
"ServerResource",
|
||||
"SubscriptionResource",
|
||||
"VersionResource",
|
||||
"WorkspaceResource",
|
||||
]
|
||||
|
||||
@@ -2,13 +2,18 @@ from typing import List, Optional
|
||||
|
||||
from gql import gql
|
||||
|
||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
|
||||
from specklepy.core.api.inputs.user_inputs import (
|
||||
UserProjectsFilter,
|
||||
UserUpdateInput,
|
||||
UserWorkspacesFilter,
|
||||
)
|
||||
from specklepy.core.api.models import (
|
||||
PendingStreamCollaborator,
|
||||
Project,
|
||||
ResourceCollection,
|
||||
User,
|
||||
)
|
||||
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
|
||||
from specklepy.core.api.resource import ResourceBase
|
||||
from specklepy.core.api.responses import DataResponse
|
||||
from specklepy.logging.exceptions import GraphQLException
|
||||
@@ -190,3 +195,146 @@ class ActiveUserResource(ResourceBase):
|
||||
)
|
||||
|
||||
return response.data.data
|
||||
|
||||
def can_create_personal_projects(self) -> PermissionCheckResult:
|
||||
QUERY = gql(
|
||||
"""
|
||||
query CanCreatePersonalProject {
|
||||
data:activeUser {
|
||||
data:permissions {
|
||||
data:canCreatePersonalProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
response = self.make_request_and_parse_response(
|
||||
DataResponse[Optional[DataResponse[DataResponse[PermissionCheckResult]]]],
|
||||
QUERY,
|
||||
)
|
||||
|
||||
if response.data is None:
|
||||
raise GraphQLException(
|
||||
"GraphQL response indicated that the ActiveUser could not be found"
|
||||
)
|
||||
|
||||
return response.data.data.data
|
||||
|
||||
def get_workspaces(
|
||||
self,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
filter: Optional[UserWorkspacesFilter] = None,
|
||||
) -> ResourceCollection[Workspace]:
|
||||
"""
|
||||
This feature is only available on Workspace enabled servers (server versions
|
||||
>=2.23.17) e.g. app.speckle.systems
|
||||
"""
|
||||
QUERY = gql(
|
||||
"""
|
||||
query ActiveUser($limit: Int!, $cursor: String, $filter: UserWorkspacesFilter) {
|
||||
data:activeUser {
|
||||
data:workspaces(limit: $limit, cursor: $cursor, filter: $filter) {
|
||||
cursor
|
||||
totalCount
|
||||
items {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
variables = {
|
||||
"limit": limit,
|
||||
"cursor": cursor,
|
||||
"filter": filter.model_dump(warnings="error", by_alias=True)
|
||||
if filter
|
||||
else None,
|
||||
}
|
||||
|
||||
response = self.make_request_and_parse_response(
|
||||
DataResponse[Optional[DataResponse[ResourceCollection[Workspace]]]],
|
||||
QUERY,
|
||||
variables,
|
||||
)
|
||||
|
||||
if response.data is None:
|
||||
raise GraphQLException(
|
||||
"GraphQL response indicated that the ActiveUser could not be found"
|
||||
)
|
||||
|
||||
return response.data.data
|
||||
|
||||
def get_active_workspace(self) -> Optional[Workspace]:
|
||||
"""
|
||||
This feature is only available on Workspace enabled servers (server versions
|
||||
>=2.23.17) e.g. app.speckle.systems
|
||||
"""
|
||||
QUERY = gql(
|
||||
"""
|
||||
query ActiveUser {
|
||||
data:activeUser {
|
||||
data:activeWorkspace {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
response = self.make_request_and_parse_response(
|
||||
DataResponse[Optional[DataResponse[Optional[Workspace]]]],
|
||||
QUERY,
|
||||
)
|
||||
|
||||
if response.data is None:
|
||||
raise GraphQLException(
|
||||
"GraphQL response indicated that the ActiveUser could not be found"
|
||||
)
|
||||
|
||||
return response.data.data
|
||||
|
||||
@@ -7,8 +7,10 @@ from specklepy.core.api.inputs.project_inputs import (
|
||||
ProjectModelsFilter,
|
||||
ProjectUpdateInput,
|
||||
ProjectUpdateRoleInput,
|
||||
WorkspaceProjectCreateInput,
|
||||
)
|
||||
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
|
||||
from specklepy.core.api.models.current import ProjectPermissionChecks
|
||||
from specklepy.core.api.resource import ResourceBase
|
||||
from specklepy.core.api.responses import DataResponse
|
||||
|
||||
@@ -55,6 +57,36 @@ class ProjectResource(ResourceBase):
|
||||
DataResponse[Project], QUERY, variables
|
||||
).data
|
||||
|
||||
def get_permissions(self, project_id: str) -> ProjectPermissionChecks:
|
||||
QUERY = gql(
|
||||
"""
|
||||
query Project($projectId: String!) {
|
||||
data:project(id: $projectId) {
|
||||
data:permissions {
|
||||
canCreateModel {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
canDelete {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
variables = {
|
||||
"projectId": project_id,
|
||||
}
|
||||
|
||||
return self.make_request_and_parse_response(
|
||||
DataResponse[DataResponse[ProjectPermissionChecks]], QUERY, variables
|
||||
).data.data
|
||||
|
||||
def get_with_models(
|
||||
self,
|
||||
project_id: str,
|
||||
@@ -198,6 +230,12 @@ class ProjectResource(ResourceBase):
|
||||
).data
|
||||
|
||||
def create(self, input: ProjectCreateInput) -> Project:
|
||||
"""
|
||||
Creates a non-workspace project (aka Personal Project)
|
||||
|
||||
see client.active_user.can_create_personal_projects to see if the user has
|
||||
permission
|
||||
"""
|
||||
QUERY = gql(
|
||||
"""
|
||||
mutation ProjectCreate($input: ProjectCreateInput) {
|
||||
@@ -227,6 +265,45 @@ class ProjectResource(ResourceBase):
|
||||
DataResponse[DataResponse[Project]], QUERY, variables
|
||||
).data.data
|
||||
|
||||
def create_in_workspace(self, input: WorkspaceProjectCreateInput) -> Project:
|
||||
"""
|
||||
Creates a workspace project
|
||||
This feature is only supported by Workspace Enabled Servers
|
||||
(e.g. app.speckle.systems)
|
||||
|
||||
see `workspace.permissions.can_create_project` to see if the user has permission
|
||||
"""
|
||||
QUERY = gql(
|
||||
"""
|
||||
mutation WorkspaceProjectCreate($input: WorkspaceProjectCreateInput!) {
|
||||
data:workspaceMutations {
|
||||
data:projects {
|
||||
data:create(input: $input) {
|
||||
id
|
||||
name
|
||||
description
|
||||
visibility
|
||||
allowPublicComments
|
||||
role
|
||||
createdAt
|
||||
updatedAt
|
||||
sourceApps
|
||||
workspaceId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
variables = {
|
||||
"input": input.model_dump(warnings="error", by_alias=True),
|
||||
}
|
||||
|
||||
return self.make_request_and_parse_response(
|
||||
DataResponse[DataResponse[DataResponse[Project]]], QUERY, variables
|
||||
).data.data.data
|
||||
|
||||
def update(self, input: ProjectUpdateInput) -> Project:
|
||||
QUERY = gql(
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple
|
||||
|
||||
import requests
|
||||
from gql import gql
|
||||
|
||||
from specklepy.core.api.models import ServerInfo
|
||||
@@ -38,11 +37,6 @@ class ServerResource(ResourceBase):
|
||||
adminContact
|
||||
canonicalUrl
|
||||
version
|
||||
roles {
|
||||
name
|
||||
description
|
||||
resourceTarget
|
||||
}
|
||||
scopes {
|
||||
name
|
||||
description
|
||||
@@ -52,6 +46,9 @@ class ServerResource(ResourceBase):
|
||||
name
|
||||
icon
|
||||
}
|
||||
workspaces {
|
||||
workspacesEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -60,16 +57,6 @@ class ServerResource(ResourceBase):
|
||||
server_info = self.make_request(
|
||||
query=query, return_type="serverInfo", schema=ServerInfo
|
||||
)
|
||||
if isinstance(server_info, ServerInfo) and isinstance(
|
||||
server_info.canonical_url, str
|
||||
):
|
||||
r = requests.get(
|
||||
server_info.canonical_url, 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
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
from typing import Optional
|
||||
|
||||
from gql import gql
|
||||
|
||||
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
|
||||
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
|
||||
from specklepy.core.api.resource import ResourceBase
|
||||
from specklepy.core.api.responses import DataResponse
|
||||
|
||||
NAME = "workspace"
|
||||
|
||||
|
||||
class WorkspaceResource(ResourceBase):
|
||||
"""API Access class for models"""
|
||||
|
||||
def __init__(self, account, basepath, client, server_version) -> None:
|
||||
super().__init__(
|
||||
account=account,
|
||||
basepath=basepath,
|
||||
client=client,
|
||||
name=NAME,
|
||||
server_version=server_version,
|
||||
)
|
||||
|
||||
def get(self, workspace_id: str) -> Workspace:
|
||||
QUERY = gql(
|
||||
"""
|
||||
query WorkspaceGet($workspaceId: String!) {
|
||||
data:workspace(id: $workspaceId) {
|
||||
id
|
||||
name
|
||||
role
|
||||
slug
|
||||
logo
|
||||
createdAt
|
||||
updatedAt
|
||||
readOnly
|
||||
description
|
||||
creationState
|
||||
{
|
||||
completed
|
||||
}
|
||||
permissions {
|
||||
canCreateProject {
|
||||
authorized
|
||||
code
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
variables = {
|
||||
"workspaceId": workspace_id,
|
||||
}
|
||||
|
||||
return self.make_request_and_parse_response(
|
||||
DataResponse[Workspace], QUERY, variables
|
||||
).data
|
||||
|
||||
def get_projects(
|
||||
self,
|
||||
workspace_id: str,
|
||||
limit: int = 25,
|
||||
cursor: Optional[str] = None,
|
||||
filter: Optional[WorksaceProjectsFilter] = None,
|
||||
) -> ResourceCollection[Project]:
|
||||
QUERY = gql(
|
||||
"""
|
||||
query Workspace($workspaceId: String!, $limit: Int!, $cursor: String, $filter: WorkspaceProjectsFilter) {
|
||||
data:workspace(id: $workspaceId) {
|
||||
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
|
||||
cursor
|
||||
items {
|
||||
allowPublicComments
|
||||
createdAt
|
||||
description
|
||||
id
|
||||
name
|
||||
role
|
||||
sourceApps
|
||||
updatedAt
|
||||
visibility
|
||||
workspaceId
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
""" # noqa: E501
|
||||
)
|
||||
|
||||
variables = {
|
||||
"workspaceId": workspace_id,
|
||||
"limit": limit,
|
||||
"cursor": cursor,
|
||||
"filter": filter.model_dump(warnings="error", by_alias=True)
|
||||
if filter
|
||||
else None,
|
||||
}
|
||||
|
||||
return self.make_request_and_parse_response(
|
||||
DataResponse[DataResponse[ResourceCollection[Project]]], QUERY, variables
|
||||
).data.data
|
||||
@@ -58,3 +58,8 @@ class UnsupportedException(SpeckleException):
|
||||
class SpeckleWarning(Warning):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
|
||||
|
||||
class WorkspacePermissionException(SpeckleException):
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(message=message)
|
||||
|
||||
@@ -13,12 +13,13 @@ class Mesh(
|
||||
IHasVolume,
|
||||
IHasUnits,
|
||||
speckle_type="Objects.Geometry.Mesh",
|
||||
detachable={"vertices", "faces", "colors", "textureCoordinates"},
|
||||
detachable={"vertices", "faces", "colors", "textureCoordinates", "vertexNormals"},
|
||||
chunkable={
|
||||
"vertices": 31250,
|
||||
"faces": 62500,
|
||||
"colors": 62500,
|
||||
"textureCoordinates": 31250,
|
||||
"vertexNormals": 31250,
|
||||
},
|
||||
serialize_ignore={"vertices_count", "texture_coordinates_count"},
|
||||
):
|
||||
@@ -31,6 +32,7 @@ class Mesh(
|
||||
faces: List[int]
|
||||
colors: List[int] = field(default_factory=list)
|
||||
textureCoordinates: List[float] = field(default_factory=list)
|
||||
vertexNormals: List[float] = field(default_factory=list)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
|
||||
@@ -4,6 +4,7 @@ from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
|
||||
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
|
||||
from specklepy.core.api.models import ResourceCollection, User
|
||||
from specklepy.logging.exceptions import GraphQLException
|
||||
|
||||
|
||||
@pytest.mark.run()
|
||||
@@ -61,3 +62,24 @@ class TestActiveUserResource:
|
||||
assert len(res.items) == 1
|
||||
assert res.total_count == 1
|
||||
assert res.items[0].id == p1.id
|
||||
|
||||
def test_can_create_personal_projects(self, client: SpeckleClient):
|
||||
res = client.active_user.can_create_personal_projects()
|
||||
res.ensure_authorised()
|
||||
|
||||
assert res.authorized is True
|
||||
|
||||
def test_get_workspaces(self, client: SpeckleClient):
|
||||
"""
|
||||
Test server is not workspace enabled, so we can't really test everything here
|
||||
We'll just test client's error handling
|
||||
"""
|
||||
with pytest.raises(GraphQLException):
|
||||
_ = client.active_user.get_workspaces()
|
||||
|
||||
def test_get_active_workspace(self, client: SpeckleClient):
|
||||
"""
|
||||
Test server is not workspace enabled, so we can't really test everything here
|
||||
"""
|
||||
res = client.active_user.get_active_workspace()
|
||||
assert res is None
|
||||
|
||||
@@ -7,6 +7,7 @@ from specklepy.core.api.inputs.project_inputs import (
|
||||
ProjectUpdateInput,
|
||||
)
|
||||
from specklepy.core.api.models import Project
|
||||
from specklepy.core.api.models.current import ProjectPermissionChecks
|
||||
from specklepy.logging.exceptions import GraphQLException
|
||||
|
||||
|
||||
@@ -65,6 +66,15 @@ class TestProjectResource:
|
||||
assert result.visibility == test_project.visibility
|
||||
assert result.created_at == test_project.created_at
|
||||
|
||||
def test_project_get_permissions(
|
||||
self, client: SpeckleClient, test_project: Project
|
||||
):
|
||||
result = client.project.get_permissions(test_project.id)
|
||||
|
||||
assert isinstance(result, ProjectPermissionChecks)
|
||||
assert result.can_create_model.authorized is True
|
||||
assert result.can_delete.authorized is True
|
||||
|
||||
def test_project_update(self, client: SpeckleClient, test_project: Project):
|
||||
new_name = "MY new name"
|
||||
new_description = "MY new desc"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.logging.exceptions import GraphQLException
|
||||
|
||||
|
||||
@pytest.mark.run()
|
||||
class TestWorkspaceResource:
|
||||
def test_get_workspace(self, client: SpeckleClient):
|
||||
"""
|
||||
Test server is not workspace enabled, so we can't really test everything here
|
||||
We'll just test client's error handling
|
||||
"""
|
||||
with pytest.raises(GraphQLException):
|
||||
client.workspace.get("not a real id")
|
||||
|
||||
def test_get_projects(self, client: SpeckleClient):
|
||||
"""
|
||||
Test server is not workspace enabled, so we can't really test everything here
|
||||
We'll just test client's error handling
|
||||
"""
|
||||
with pytest.raises(GraphQLException):
|
||||
client.workspace.get_projects("not a real id")
|
||||
@@ -151,6 +151,7 @@ def test_arc_serialization(sample_arc):
|
||||
serialized = serialize(sample_arc)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Arc)
|
||||
assert deserialized.startPoint.x == sample_arc.startPoint.x
|
||||
assert deserialized.startPoint.y == sample_arc.startPoint.y
|
||||
assert deserialized.startPoint.z == sample_arc.startPoint.z
|
||||
@@ -116,6 +116,7 @@ def test_box_serialization(sample_box):
|
||||
serialized = serialize(sample_box)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Box)
|
||||
assert deserialized.basePlane.origin.x == sample_box.basePlane.origin.x
|
||||
assert deserialized.basePlane.origin.y == sample_box.basePlane.origin.y
|
||||
assert deserialized.basePlane.origin.z == sample_box.basePlane.origin.z
|
||||
@@ -107,6 +107,7 @@ def test_circle_serialization(sample_circle):
|
||||
serialized = serialize(sample_circle)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Circle)
|
||||
assert deserialized.plane.origin.x == sample_circle.plane.origin.x
|
||||
assert deserialized.plane.origin.y == sample_circle.plane.origin.y
|
||||
assert deserialized.plane.origin.z == sample_circle.plane.origin.z
|
||||
+1
@@ -70,6 +70,7 @@ def test_control_point_serialization(sample_control_point):
|
||||
serialized = serialize(sample_control_point)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, ControlPoint)
|
||||
assert deserialized.x == sample_control_point.x
|
||||
assert deserialized.y == sample_control_point.y
|
||||
assert deserialized.z == sample_control_point.z
|
||||
@@ -125,6 +125,7 @@ def test_curve_serialization(sample_curve):
|
||||
serialized = serialize(sample_curve)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Curve)
|
||||
assert deserialized.degree == sample_curve.degree
|
||||
assert deserialized.periodic == sample_curve.periodic
|
||||
assert deserialized.rational == sample_curve.rational
|
||||
@@ -104,6 +104,7 @@ def test_ellipse_serialization(sample_ellipse):
|
||||
serialized = serialize(sample_ellipse)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Ellipse)
|
||||
assert deserialized.plane.origin.x == sample_ellipse.plane.origin.x
|
||||
assert deserialized.plane.origin.y == sample_ellipse.plane.origin.y
|
||||
assert deserialized.plane.origin.z == sample_ellipse.plane.origin.z
|
||||
@@ -83,6 +83,7 @@ def test_line_serialization(sample_line):
|
||||
serialized = serialize(sample_line)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Line)
|
||||
assert deserialized.start.x == sample_line.start.x
|
||||
assert deserialized.start.y == sample_line.start.y
|
||||
assert deserialized.start.z == sample_line.start.z
|
||||
@@ -155,6 +155,7 @@ def test_mesh_creation(cube_vertices, cube_faces):
|
||||
assert mesh.faces == cube_faces
|
||||
assert mesh.colors == []
|
||||
assert mesh.textureCoordinates == []
|
||||
assert mesh.vertexNormals == []
|
||||
assert mesh.units == Units.m.value
|
||||
|
||||
|
||||
@@ -239,6 +240,7 @@ def test_mesh_serialization(full_mesh):
|
||||
serialized = serialize(full_mesh)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Mesh)
|
||||
assert deserialized.vertices == full_mesh.vertices
|
||||
assert deserialized.faces == full_mesh.faces
|
||||
assert deserialized.colors == full_mesh.colors
|
||||
@@ -111,6 +111,7 @@ def test_spiral_serialization(sample_spiral: Spiral):
|
||||
serialized = serialize(sample_spiral)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Spiral)
|
||||
assert deserialized.start_point.x == sample_spiral.start_point.x
|
||||
assert deserialized.start_point.y == sample_spiral.start_point.y
|
||||
assert deserialized.start_point.z == sample_spiral.start_point.z
|
||||
@@ -30,6 +30,7 @@ def test_point_serialization():
|
||||
serialized = serialize(p1)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Point)
|
||||
assert deserialized.x == p1.x
|
||||
assert deserialized.y == p1.y
|
||||
assert deserialized.z == p1.z
|
||||
@@ -47,6 +47,7 @@ def test_point_cloud_serialization(sample_point_cloud):
|
||||
serialized = serialize(sample_point_cloud)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, PointCloud)
|
||||
assert len(deserialized.points) == len(sample_point_cloud.points)
|
||||
|
||||
for orig_point, deserial_point in zip(
|
||||
@@ -86,15 +86,21 @@ def test_polycurve_serialization(sample_polycurve: Polycurve):
|
||||
serialized = serialize(sample_polycurve)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Polycurve)
|
||||
assert len(deserialized.segments) == len(sample_polycurve.segments)
|
||||
assert deserialized.units == sample_polycurve.units
|
||||
|
||||
assert deserialized.segments[0].start.x == sample_polycurve.segments[0].start.x
|
||||
assert deserialized.segments[0].start.y == sample_polycurve.segments[0].start.y
|
||||
assert deserialized.segments[0].start.z == sample_polycurve.segments[0].start.z
|
||||
assert deserialized.segments[0].end.x == sample_polycurve.segments[0].end.x
|
||||
assert deserialized.segments[0].end.y == sample_polycurve.segments[0].end.y
|
||||
assert deserialized.segments[0].end.z == sample_polycurve.segments[0].end.z
|
||||
expectedSegment = sample_polycurve.segments[0]
|
||||
segment = deserialized.segments[0]
|
||||
assert isinstance(expectedSegment, Line)
|
||||
assert isinstance(segment, Line)
|
||||
|
||||
assert segment.start.x == expectedSegment.start.x
|
||||
assert segment.start.y == expectedSegment.start.y
|
||||
assert segment.start.z == expectedSegment.start.z
|
||||
assert segment.end.x == expectedSegment.end.x
|
||||
assert segment.end.y == expectedSegment.end.y
|
||||
assert segment.end.z == expectedSegment.end.z
|
||||
|
||||
|
||||
def test_polycurve_empty():
|
||||
@@ -134,6 +134,7 @@ def test_polyline_serialization(sample_polyline):
|
||||
serialized = serialize(sample_polyline)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Polyline)
|
||||
assert deserialized.value == sample_polyline.value
|
||||
assert deserialized.units == sample_polyline.units
|
||||
assert deserialized.domain.start == sample_polyline.domain.start
|
||||
@@ -69,6 +69,7 @@ def test_region_serialization(sample_region):
|
||||
serialized = serialize(sample_region)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Region)
|
||||
assert deserialized.hasHatchPattern == sample_region.hasHatchPattern
|
||||
assert deserialized.units == sample_region.units
|
||||
|
||||
@@ -113,6 +113,7 @@ def test_spiral_serialization(sample_spiral: Spiral):
|
||||
serialized = serialize(sample_spiral)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Spiral)
|
||||
assert deserialized.start_point.x == sample_spiral.start_point.x
|
||||
assert deserialized.start_point.y == sample_spiral.start_point.y
|
||||
assert deserialized.start_point.z == sample_spiral.start_point.z
|
||||
@@ -140,6 +140,7 @@ def test_surface_serialization(sample_surface: Surface):
|
||||
serialized = serialize(sample_surface)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Surface)
|
||||
assert deserialized.degreeU == sample_surface.degreeU
|
||||
assert deserialized.degreeV == sample_surface.degreeV
|
||||
assert deserialized.rational == sample_surface.rational
|
||||
@@ -36,6 +36,7 @@ def test_vector_serialization():
|
||||
serialized = serialize(v)
|
||||
deserialized = deserialize(serialized)
|
||||
|
||||
assert isinstance(deserialized, Vector)
|
||||
assert deserialized.x == v.x
|
||||
assert deserialized.y == v.y
|
||||
assert deserialized.z == v.z
|
||||
Reference in New Issue
Block a user