Compare commits

...

18 Commits

Author SHA1 Message Date
Dogukan Karatas add470699b Merge pull request #314 from specklesystems/dogukan/cnx-3036-blender
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: add model and version permission check
2026-02-02 14:32:18 +01:00
Dogukan Karatas f5bcd805e8 Merge branch 'v3-dev' into dogukan/cnx-3036-blender 2026-02-02 14:30:38 +01:00
Dogukan Karatas cf4bb14240 Merge pull request #316 from specklesystems/dogukan/cnx-3003-blender
feat: add model ingestion support
2026-02-02 14:26:42 +01:00
Dogukan Karatas 1ee1650fef error messages implemented 2026-02-02 13:05:44 +01:00
Dogukan Karatas 70f5f672a6 Merge branch 'v3-dev' into dogukan/cnx-3003-blender 2026-02-02 09:57:46 +01:00
Dogukan Karatas b329ec8c97 Merge pull request #315 from specklesystems/dogukan/cnx-2996-blender
feat: remove personal projects
2026-02-02 09:57:24 +01:00
bimgeek b54cfe16e8 fix result version id 2026-01-30 21:54:17 +03:00
bimgeek 357859827c fix model ingestion inputs 2026-01-30 21:47:38 +03:00
Dogukan Karatas f35457dff8 pass file size 2026-01-30 17:11:06 +01:00
Dogukan Karatas f993c38ea9 selection dialog fix 2026-01-30 16:59:30 +01:00
Dogukan Karatas 624537cc5d implements model ingestion check 2026-01-30 12:33:16 +01:00
Dogukan Karatas ebb7f1b3bf formatted 2026-01-30 09:12:38 +01:00
Dogukan Karatas ac2a95d968 removes personal projects 2026-01-30 09:08:29 +01:00
Dogukan Karatas 2440c44f44 formatted 2026-01-29 17:21:43 +01:00
Dogukan Karatas 33dfa1229c can create version checks 2026-01-29 16:37:35 +01:00
Jedd Morgan ea61bd06b8 feat(receive): Call MarkVersionReceived (#312)
* Enable ruff pre-commit

* bump ruff

* Call `client.version.received`
2026-01-27 09:35:43 +00:00
Jedd Morgan e071aca299 Update readme with local dev instructions (#311) 2026-01-26 15:03:58 +00:00
Jedd Morgan 4a8a980034 chore(ci): enable pre-commit (#309)
* Enable ruff pre-commit

* bump ruff
2026-01-26 15:03:07 +00:00
22 changed files with 1181 additions and 649 deletions
+1 -1
View File
@@ -14,4 +14,4 @@ workflows:
when:
false
jobs:
- build
- build
+6 -6
View File
@@ -19,13 +19,13 @@ jobs:
- 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') }}
- 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 pre-commit
run: uv run pre-commit run --all-files
- name: Minimize uv cache
run: uv cache prune --ci
+1 -1
View File
@@ -14,4 +14,4 @@ modules/
.tool-versions
requirements.txt
SEMVER
dui3/
dui3/
+21
View File
@@ -0,0 +1,21 @@
repos:
- repo: local
hooks:
# Run the linter.
- id: ruff
name: ruff lint
entry: uv run ruff check --force-exclude
language: system
types_or: [python, pyi]
# Run the formatter.
- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types_or: [python, pyi]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+36 -6
View File
@@ -39,16 +39,16 @@ Give Speckle a try in no time by:
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-docs.speckle.systems-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://docs.speckle.systems/connectors/blender) reference on almost any end-user and developer functionality
# Blender Connector
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
## Installation
We officially support Blender 4.2 and newer, on Windows.
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
@@ -62,7 +62,6 @@ The Speckle connector UI can be found in the 3d viewport toolbar (N), under the
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows.
@@ -77,6 +76,40 @@ If you find an addon that conflicts, please try using a different version of tha
If you can't find a version of an addon that works, please let us know on [our forums](https://speckle.community/) the name of the addon, the versions you've tried, the version of the Speckle connector you've tried, and your OS (win/mac/linux).
## Local Development
Pre-resquisits:
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- A supported [blender](https://www.blender.org/download/) version
- [Blender Development](https://marketplace.visualstudio.com/items?itemName=JacquesLucke.blender-development) extension for VS Code (recommended)
First time setup (or anytime you change pyproject.toml)
Run the following commands
```sh
uv sync
./export_dependencies.sh
```
🪟 To activate the environment in a terminal (Windows):
```powershell
.venv\Scripts\activate
```
🐧 To activate the environment in a terminal (Linux / macOS):
```sh
source .venv/bin/activate.fish
```
---
To run the blender plugin, run the `>Blender: Start`
from VS code (`ctrl + shift + p`)
<img width="1469" height="379" alt="image" src="https://github.com/user-attachments/assets/9dc174a0-07fc-47c7-85d1-bd5a04d8f8c7" />
## Contributing
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
@@ -88,6 +121,3 @@ The Speckle Community hangs out on [the forum](https://discourse.speckle.works),
## License
Unless otherwise described, the code in this repository is licensed under the Apache-2.0 License. Please note that some modules, extensions or code herein might be otherwise licensed. This is indicated either in the root of the containing folder under a different license file, or in the respective file's header. If you have any questions, don't hesitate to get in touch with us via [email](mailto:hello@speckle.systems).
## Notes
Thanks to [Tom Svilans](http://tomsvilans.com) ([Github](https://github.com/tsvilans)) for the original v1 contribution!
+1 -1
View File
@@ -231,7 +231,7 @@ def unregister():
# Clear any pending timers to prevent duplicate calls
if bpy.app.timers.is_registered(delayed_version_check):
bpy.app.timers.unregister(delayed_version_check)
icons.unload_icons()
unregister_speckle_state() # Unregister SpeckleState
_client_cache.clear()
@@ -3,7 +3,7 @@ from bpy.types import Context, Event, UILayout
from specklepy.core.api.inputs import CreateModelInput
from typing import Tuple
from ..utils.account_manager import _client_cache
from ..utils.account_manager import _client_cache, can_create_model
class SPECKLE_OT_create_model(bpy.types.Operator):
@@ -11,11 +11,26 @@ class SPECKLE_OT_create_model(bpy.types.Operator):
bl_label = "Create Model"
bl_description = "Create a new Speckle model"
_can_create: bool = True
model_name: bpy.props.StringProperty(name="Model Name") # type: ignore
@classmethod
def description(cls, context: Context, properties) -> str:
if not cls._can_create:
return "Workspace limits have been reached"
return "Create a new Speckle model"
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
authorized, auth_message = can_create_model(
wm.selected_account_id, wm.selected_project_id
)
if not authorized:
self.report({"ERROR"}, auth_message)
return {"CANCELLED"}
if not self.model_name.strip():
self.report({"ERROR"}, "Model name cannot be empty")
return {"CANCELLED"}
@@ -1,10 +1,9 @@
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.inputs import ProjectCreateInput
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
from specklepy.core.api.enums import ProjectVisibility
from typing import Tuple, Optional
from typing import Tuple
from ..utils.account_manager import _client_cache
@@ -25,9 +24,7 @@ class SPECKLE_OT_create_project(bpy.types.Operator):
project_id, project_name = create_project(
wm.selected_account_id,
self.project_name,
None
if wm.selected_workspace.id == "personal"
else wm.selected_workspace.id,
wm.selected_workspace.id,
)
wm.selected_project_id = project_id
wm.selected_project_name = project_name
@@ -54,30 +51,21 @@ def unregister() -> None:
def create_project(
account_id: str, project_name: str, workspace_id: Optional[str]
account_id: str, project_name: str, workspace_id: str
) -> Tuple[str, str]:
try:
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
raise Exception(f"Could not get client for account: {account_id}")
if workspace_id:
project = client.project.create_in_workspace(
input=WorkspaceProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility("PUBLIC"),
workspaceId=workspace_id,
)
)
else:
project = client.project.create(
input=ProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility("PUBLIC"),
)
project = client.project.create_in_workspace(
input=WorkspaceProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility("PUBLIC"),
workspaceId=workspace_id,
)
)
return (project.id, project.name)
except Exception as e:
@@ -2,6 +2,7 @@ import bpy
from typing import Set
from bpy.types import Context, Event
from ..operations.publish_operation import publish_operation
from ..utils.account_manager import can_create_version
class SPECKLE_OT_publish_model_card(bpy.types.Operator):
@@ -27,6 +28,14 @@ class SPECKLE_OT_publish_model_card(bpy.types.Operator):
self.model_card_id
)
# On-demand permission check
authorized, auth_message = can_create_version(
model_card.account_id, model_card.project_id, model_card.model_id
)
if not authorized:
self.report({"ERROR"}, auth_message)
return {"CANCELLED"}
# set wm
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
@@ -4,7 +4,7 @@ from bpy.types import Event
from typing import Set
from ..operations.publish_operation import publish_operation
from ..utils.account_manager import get_server_url_by_account_id
from ..utils.account_manager import get_server_url_by_account_id, can_create_version
from ..utils.model_card_utils import model_card_exists, update_model_card_objects
@@ -55,6 +55,11 @@ class SPECKLE_OT_publish(bpy.types.Operator):
self.report({"ERROR"}, "No model selected")
return {"CANCELLED"}
authorized, auth_message = can_create_version(account_id, project_id, model_id)
if not authorized:
self.report({"ERROR"}, auth_message)
return {"CANCELLED"}
objects_to_convert = []
for speckle_obj in wm.speckle_objects:
blender_obj = bpy.data.objects.get(speckle_obj.name)
@@ -1,29 +1,29 @@
from typing import Dict, Union
import bpy
from bpy.types import Context
from specklepy.transports.server import ServerTransport
from specklepy.core.api import operations
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.core.api import host_applications, operations
from specklepy.core.api.inputs.version_inputs import MarkReceivedVersionInput
from specklepy.logging import metrics
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.core.api import host_applications
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.transports.server import ServerTransport
from ..utils.get_ascendants import get_ascendants
from ..utils.account_manager import _client_cache
from ...converter.utils import (
find_object_by_id,
get_project_workspace_id,
build_object_id_map,
)
from ... import bl_info
from ...converter.to_native import (
convert_to_native,
render_material_proxy_to_native,
instance_definition_proxy_to_native,
find_instance_definitions,
instance_definition_proxy_to_native,
render_material_proxy_to_native,
)
from specklepy.logging import metrics
from ... import bl_info
from typing import Dict, Union
from ...converter.utils import (
build_object_id_map,
get_project_workspace_id,
)
from ..utils.account_manager import _client_cache
from ..utils.get_ascendants import get_ascendants
def load_operation(
@@ -34,53 +34,50 @@ def load_operation(
"""
wm = context.window_manager
accountId: str = wm.selected_account_id # type: ignore
projectId: str = wm.selected_project_id # type: ignore
versionId: str = wm.selected_version_id # type: ignore
# get cached client
client = _client_cache.get_client(context.window_manager.selected_account_id)
client = _client_cache.get_client(accountId)
if not client:
print("No Speckle client found")
return {}
print(f"Using client for account: {context.window_manager.selected_account_id}")
print(f"Using client for account: {accountId}")
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
transport = ServerTransport(stream_id=projectId, client=client)
version = client.version.get(wm.selected_version_id, wm.selected_project_id)
version = client.version.get(versionId, projectId)
obj_id = version.referenced_object
if not obj_id:
raise ValueError("Unable to receive version beyond workspaces limit")
version_data = operations.receive(obj_id, transport)
metrics.set_host_app("blender")
# Get account for metrics tracking
from specklepy.core.api.credentials import get_local_accounts
account = next(
(
acc
for acc in get_local_accounts()
if acc.id == context.window_manager.selected_account_id
),
None,
client.version.received(
MarkReceivedVersionInput(
version_id=version.id,
project_id=projectId,
source_application="blender",
)
)
if account:
metrics.track(
metrics.RECEIVE,
account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != account.userInfo.id,
"workspace_id": get_project_workspace_id(
client, wm.selected_project_id
),
},
)
metrics.track(
metrics.RECEIVE,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
},
)
# Build object ID map once
object_id_map = build_object_id_map(version_data)
@@ -8,6 +8,14 @@ from specklepy.core.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.objects.models.units import Units
from specklepy.logging.exceptions import GraphQLException, WorkspacePermissionException
from specklepy.core.api.inputs.model_ingestion_inputs import (
ModelIngestionCreateInput,
ModelIngestionStartProcessingInput,
ModelIngestionSuccessInput,
ModelIngestionFailedInput,
SourceDataInput,
)
from ...converter.to_speckle import convert_to_speckle
from ...converter.to_speckle.material_to_speckle import (
@@ -19,6 +27,108 @@ from specklepy.logging import metrics
from ... import bl_info
def _check_use_model_ingestion_send(client, project_id: str, model_id: str) -> bool:
"""Check if the server supports model ingestion and the user is authorized."""
try:
result = client.model.can_create_model_ingestion(project_id, model_id)
result.ensure_authorised()
return True
except GraphQLException:
return False
def _build_source_data() -> SourceDataInput:
"""Build data input for model ingestion."""
file_name = bpy.path.basename(bpy.data.filepath)
if not file_name:
file_name = "Untitled.blend"
file_size_bytes: Optional[int] = None
if bpy.data.filepath:
import os
try:
file_size_bytes = os.path.getsize(bpy.data.filepath)
except OSError:
pass
blender_version = ".".join(map(str, bl_info["blender"]))
return SourceDataInput(
source_application_slug="blender",
source_application_version=blender_version,
file_name=file_name,
file_size_bytes=file_size_bytes,
)
def _send_via_ingestion(
client,
project_id: str,
model_id: str,
obj_id: str,
version_message: str,
) -> str:
"""Send via the model ingestion. Returns version_id."""
source_data = _build_source_data()
create_input = ModelIngestionCreateInput(
project_id=project_id,
model_id=model_id,
source_data=source_data,
progress_message="Model ingestion created",
)
ingestion = client.model_ingestion.create(create_input)
ingestion_id = ingestion.id
try:
start_input = ModelIngestionStartProcessingInput(
project_id=project_id,
ingestion_id=ingestion_id,
progress_message="Processing model ingestion",
source_data=source_data,
)
client.model_ingestion.start_processing(start_input)
success_input = ModelIngestionSuccessInput(
project_id=project_id,
ingestion_id=ingestion_id,
root_object_id=obj_id,
version_message=version_message,
)
version_id = client.model_ingestion.complete(success_input)
return version_id
except Exception:
try:
fail_input = ModelIngestionFailedInput(
project_id=project_id,
ingestion_id=ingestion_id,
error_reason="Failed during processing",
)
client.model_ingestion.fail_with_error(fail_input)
except Exception:
pass
raise
def _send_via_version_create(
client,
project_id: str,
model_id: str,
obj_id: str,
version_message: str,
) -> str:
"""Send via the legacy version.create() flow. Returns version_id."""
version_input = CreateVersionInput(
objectId=obj_id,
modelId=model_id,
projectId=project_id,
message=version_message,
sourceApplication="blender",
)
version = client.version.create(version_input)
return version.id
def publish_operation(
context: Context,
objects_to_convert: List,
@@ -36,7 +146,13 @@ def publish_operation(
if not client:
return False, "No Speckle client found", None
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
project_id = wm.selected_project_id
model_id = wm.selected_model_id
# check ingestion support before sending data (fail fast on permission errors)
use_ingestion = _check_use_model_ingestion_send(client, project_id, model_id)
transport = ServerTransport(stream_id=project_id, client=client)
# build collection hierarchy and convert objects
root_collection = build_collection_hierarchy(
@@ -51,16 +167,14 @@ def publish_operation(
obj_id = operations.send(root_collection, [transport])
version_input = CreateVersionInput(
objectId=obj_id,
modelId=wm.selected_model_id,
projectId=wm.selected_project_id,
message=version_message,
sourceApplication="blender",
)
version = client.version.create(version_input)
version_id = version.id
if use_ingestion:
version_id = _send_via_ingestion(
client, project_id, model_id, obj_id, version_message
)
else:
version_id = _send_via_version_create(
client, project_id, model_id, obj_id, version_message
)
# Get account for metrics tracking
from specklepy.core.api.credentials import get_local_accounts
@@ -80,9 +194,7 @@ def publish_operation(
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"workspace_id": get_project_workspace_id(
client, wm.selected_project_id
),
"workspace_id": get_project_workspace_id(client, project_id),
},
)
@@ -95,6 +207,9 @@ def publish_operation(
version_id,
)
except WorkspacePermissionException as e:
return False, f"Permission denied: {str(e)}", None
except Exception as e:
import traceback
@@ -126,8 +126,8 @@ def update_workspaces_list(context: Context) -> None:
active_workspace = get_active_workspace(wm.selected_account_id)
if active_workspace:
wm.selected_workspace.id = active_workspace["id"]
else:
wm.selected_workspace.id = "personal"
elif wm.speckle_workspaces:
wm.selected_workspace.id = wm.speckle_workspaces[0].id
print("Updated Workspaces List!")
@@ -2,6 +2,8 @@ import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event
from ..utils.model_manager import get_models_for_project
from ..utils.version_manager import get_latest_version
from ..utils.account_manager import can_create_model
from ..blender_operators.create_model import SPECKLE_OT_create_model
class SPECKLE_UL_models_list(bpy.types.UIList):
@@ -94,6 +96,11 @@ class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
def invoke(self, context: Context, event: Event) -> set[str]:
self.update_models_list(context)
wm = context.window_manager
authorized, _ = can_create_model(wm.selected_account_id, wm.selected_project_id)
self._can_create_model = authorized
SPECKLE_OT_create_model._can_create = authorized
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
@@ -104,7 +111,9 @@ class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
row = layout.row(align=True)
row.prop(self, "search_query", icon="VIEWZOOM", text="") # search bar
if wm.ui_mode != "LOAD":
row.operator("speckle.create_model", icon="ADD", text="")
sub = row.row(align=True)
sub.enabled = getattr(self, "_can_create_model", True)
sub.operator("speckle.create_model", icon="ADD", text="")
layout.template_list(
"SPECKLE_UL_models_list",
@@ -125,8 +125,13 @@ class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
wm.selected_workspace.id = active_workspace["id"]
wm.selected_workspace.name = active_workspace["name"]
else:
wm.selected_workspace.id = "personal"
wm.selected_workspace.name = "Personal Projects"
from .account_selection_dialog import update_workspaces_list
update_workspaces_list(context)
workspaces = list(wm.speckle_workspaces)
if workspaces:
wm.selected_workspace.id = workspaces[0].id
wm.selected_workspace.name = workspaces[0].name
# Fetch projects from server
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
@@ -3,6 +3,7 @@ from typing import List
from bpy.types import Operator, Context, Object
from bpy.props import EnumProperty
from ..utils.model_card_utils import update_model_card_objects
from ..utils.account_manager import can_create_version
class SPECKLE_OT_selection_filter_dialog(Operator):
@@ -45,6 +46,14 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
update_model_card_objects(model_card, user_selection)
self.report({"INFO"}, "Selection updated")
# On-demand permission check before publishing
authorized, auth_message = can_create_version(
model_card.account_id, model_card.project_id, model_card.model_id
)
if not authorized:
self.report({"ERROR"}, auth_message)
return {"CANCELLED"}
# Call the publish operator
bpy.ops.speckle.model_card_publish(
model_card_id=self.model_card_id, version_message=self.version_message
+45 -24
View File
@@ -95,22 +95,20 @@ def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
for ws in workspaces
if ws.creation_state is None or ws.creation_state.completed
]
personal_projects_text = "Personal Projects (Legacy)"
else:
workspace_list = []
personal_projects_text = "Personal Projects"
workspace_list.append(("personal", personal_projects_text))
if workspaces_enabled:
active_workspace = client.active_user.get_active_workspace()
default_workspace_id = (
active_workspace.id if active_workspace else "personal"
active_workspace.id
if active_workspace
else (workspaces[0].id if workspaces else None)
)
result = reorder_tuple(workspace_list, default_workspace_id)
if default_workspace_id:
result = reorder_tuple(workspace_list, default_workspace_id)
else:
result = workspace_list
else:
result = workspace_list
result = []
return result
except Exception as e:
@@ -148,7 +146,7 @@ def get_active_workspace(account_id: str) -> Optional[Dict[str, str]]:
active_workspace = client.active_user.get_active_workspace()
if active_workspace:
return {"id": active_workspace.id, "name": active_workspace.name}
return {"id": "personal", "name": "Personal Projects"}
return None
except Exception as e:
print(f"Error in get_active_workspace: {str(e)}")
_client_cache.clear()
@@ -264,16 +262,42 @@ def can_load(client, project) -> Tuple[bool, str]:
return False, error_msg
def can_publish(client, project) -> Tuple[bool, str]:
def can_create_version(
account_id: str, project_id: str, model_id: str
) -> Tuple[bool, str]:
try:
permissions = client.project.get_permissions(project.id)
client = _client_cache.get_client(account_id)
permissions = client.model.get_permissions(project_id, model_id)
if permissions.can_publish.authorized:
if permissions.can_create_version.authorized:
return True, ""
else:
message = getattr(permissions.can_create_version, "message", None)
return (
False,
"Your role on this project doesn't give you permission to publish.",
message
or "Your role on this project doesn't give you permission to publish.",
)
except Exception as e:
error_msg = f"Failed to check permissions: {str(e)}"
print(error_msg)
return False, error_msg
def can_create_model(account_id: str, project_id: str) -> Tuple[bool, str]:
try:
client = _client_cache.get_client(account_id)
permissions = client.project.get_permissions(project_id)
if permissions.can_create_model.authorized:
return True, ""
else:
message = getattr(permissions.can_create_model, "message", None)
return (
False,
message
or "You don't have permission to create models in this project.",
)
except Exception as e:
@@ -289,15 +313,12 @@ def can_create_project_in_workspace(account_id: str, workspace_id: str) -> bool:
try:
client = _client_cache.get_client(account_id)
if workspace_id == "personal":
return client.active_user.can_create_personal_projects().authorized
else:
try:
workspace = client.workspace.get(workspace_id)
return workspace.permissions.can_create_project.authorized
except Exception as e:
print(f"Failed to get workspace: {str(e)}")
return False
try:
workspace = client.workspace.get(workspace_id)
return workspace.permissions.can_create_project.authorized
except Exception as e:
print(f"Failed to get workspace: {str(e)}")
return False
except Exception as e:
print(f"Error in can_create_project_in_workspace: {str(e)}")
_client_cache.clear() # Clear cache on error
@@ -30,9 +30,6 @@ def get_projects_for_account(
print(f"Error: Could not find account with ID: {account_id}")
return []
if workspace_id == "personal":
return _get_personal_projects_with_permissions(client, search)
try:
workspace_resource = WorkspaceResource(
account, client.url, client.httpclient, client.server.version()
@@ -95,43 +92,6 @@ def get_projects_for_account(
return []
def _get_personal_projects_with_permissions(
client: SpeckleClient, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
helper function to get personal projects with permissions using the old method
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspaceId=None,
personalOnly=True,
include_implicit_access=True,
)
projects = client.active_user.get_projects(limit=10, filter=filter).items
result = []
for project in projects:
can_load_permission, _ = can_load(client, project)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
def _get_projects_with_individual_permissions(
client: SpeckleClient,
workspace_id: str,
+22 -17
View File
@@ -1,26 +1,27 @@
from typing import Any, Iterable, List, Optional, Tuple, Dict
from specklepy.objects import Base
from specklepy.objects import DataObject
from typing import Any, Dict, Iterable, List, Optional, Tuple
import bpy
import mathutils
from bpy.types import Object
from specklepy.objects import Base, DataObject
from specklepy.objects.geometry import (
Line,
Polyline,
Mesh,
Arc,
Circle,
Ellipse,
Curve,
Polycurve,
Ellipse,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
from specklepy.objects.models.units import (
get_scale_factor_to_meters,
get_units_from_string,
)
from specklepy.objects.proxies import InstanceProxy
from specklepy.objects.models.units import (
get_units_from_string,
get_scale_factor_to_meters,
)
import bpy
from bpy.types import Object
import mathutils
from ..converter.utils import create_material_from_proxy, find_object_by_id
from ..converter.utils import create_material_from_proxy
# Display value property aliases to check for
DISPLAY_VALUE_PROPERTY_ALIASES = [
@@ -714,6 +715,7 @@ def arc_to_native(
converts a Speckle arc to a Blender NURBS curve.
"""
import math
import mathutils
curve = bpy.data.curves.new(data_block_name, type="CURVE")
@@ -848,6 +850,7 @@ def circle_to_native(
converts a Speckle circle to a Blender NURBS curve.
"""
import math
import mathutils
curve = bpy.data.curves.new(data_block_name, type="CURVE")
@@ -1340,7 +1343,9 @@ def instance_definition_proxy_to_native(
if max_depth > 0: # Only process if max_depth allows
assert (
found_obj.definitionId in definition_collections
), f"Definition collection not found for nested instance {found_obj.definitionId}"
), (
f"Definition collection not found for nested instance {found_obj.definitionId}"
)
if instance_loading_mode == "LINKED_DUPLICATES":
blender_obj = instance_proxy_to_linked_duplicates(
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = "Stop"
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
+6 -2
View File
@@ -5,8 +5,12 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.2.3",
"specklepy>=3.2.4",
]
[dependency-groups]
dev = ["fake-bpy-module-latest>=20240524,<20240525", "ruff>=0.4.4,<0.5"]
dev = [
"fake-bpy-module-latest>=20260126",
"ruff==0.14.14",
"pre-commit>=4.0.1",
]
Generated
+795 -460
View File
File diff suppressed because it is too large Load Diff