Compare commits

...

3 Commits

Author SHA1 Message Date
Jedd Morgan a8a5296d7e Limited Workspace (#438)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
2025-07-24 18:30:43 +03:00
Jedd Morgan 4f82c0f43d feat(api): Added functions for fetching the connector version feeds. (#435)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* Added connector feed utility functions

* Check ex code
2025-07-18 13:00:09 +01:00
Jedd Morgan f5e024c8ce perf(serializer): Avoid unnecessary serialization of detached objects (#431)
* Avoid unnecessary serialization of detached objects

* camel case variable namings
2025-06-16 16:24:41 +01:00
6 changed files with 115 additions and 21 deletions
@@ -12,6 +12,7 @@ from specklepy.core.api.models import (
User,
)
from specklepy.core.api.models.current import (
LimitedWorkspace,
PermissionCheckResult,
ProjectWithPermissions,
Workspace,
@@ -94,7 +95,7 @@ class ActiveUserResource(CoreResource):
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]:
def get_active_workspace(self) -> Optional[LimitedWorkspace]:
metrics.track(
metrics.SDK, self.account, {"name": "Active User Get Active Workspace"}
)
@@ -0,0 +1,70 @@
from datetime import datetime
from typing import List
import httpx
from pydantic import AliasGenerator, BaseModel, ConfigDict, HttpUrl
from pydantic.alias_generators import to_pascal
class ConnectorFeedBaseModel(BaseModel):
"""
Parent class for all Connector Feed Object Model classes
Sets-up a pydantic config to serialize properties using a pascal case alias
"""
model_config = ConfigDict(
alias_generator=AliasGenerator(
validation_alias=to_pascal,
),
populate_by_name=True,
)
class ConnectorVersion(ConnectorFeedBaseModel):
number: str
url: HttpUrl
os: int # this is an enum, it's properly defined in the old v2 SDK (used by Speckle.Manager.Feed) # noqa: E501
architecture: int # These are enums, they are properly defined in the old v2 SDK (used by Speckle.Manager.Feed) # noqa: E501
date: datetime
prerelease: bool
class ConnectorVersions(ConnectorFeedBaseModel):
versions: List[ConnectorVersion]
def get_latest_version(host_app_slug: str, allow_pre_release: bool) -> ConnectorVersion:
"""
Fetches the JSON feed for the given connector slug and
Returns the latest version by date - Note, it does not consider semvers!
Arguments:
host_app_slug {str} -- the host app slug to query for
allow_pre_release {bool} -- if false, only stable releases will be considered
Raises:
HTTPStatusError: if http request failed
ValidationError: response was not valid json
ValueError: The feed contained no connector versions
"""
connector_versions = get_connector_versions(host_app_slug).versions
filtered_versions = [
v for v in connector_versions if allow_pre_release or not v.prerelease
]
return max(filtered_versions, key=lambda x: x.date)
def get_connector_versions(host_app_slug: str) -> ConnectorVersions:
"""
Fetches the JSON feed for the given slug (v3 feeds only)
Raises:
HTTPStatusError: if http request failed
ValidationError: response was not valid json
"""
url = f"https://releases.speckle.dev/manager2/feeds/{host_app_slug.lower()}-v3.json"
res = httpx.get(url).raise_for_status()
feed_data = ConnectorVersions.model_validate_json(res.text)
return feed_data
+5 -2
View File
@@ -213,15 +213,18 @@ class WorkspaceCreationState(GraphQLBaseModel):
completed: bool
class Workspace(GraphQLBaseModel):
class LimitedWorkspace(GraphQLBaseModel):
id: str
name: str
role: Optional[str]
slug: str
logo: Optional[str]
description: Optional[str]
class Workspace(LimitedWorkspace):
created_at: datetime
updated_at: datetime
read_only: bool
description: Optional[str]
creation_state: Optional[WorkspaceCreationState]
permissions: WorkspacePermissionChecks
@@ -14,6 +14,7 @@ from specklepy.core.api.models import (
User,
)
from specklepy.core.api.models.current import (
LimitedWorkspace,
PermissionCheckResult,
ProjectWithPermissions,
Workspace,
@@ -295,7 +296,7 @@ class ActiveUserResource(ResourceBase):
return response.data.data
def get_active_workspace(self) -> Optional[Workspace]:
def get_active_workspace(self) -> Optional[LimitedWorkspace]:
"""
This feature is only available on Workspace enabled servers (server versions
>=2.23.17) e.g. app.speckle.systems
@@ -310,29 +311,15 @@ class ActiveUserResource(ResourceBase):
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]]]],
DataResponse[Optional[DataResponse[Optional[LimitedWorkspace]]]],
QUERY,
)
@@ -199,8 +199,9 @@ class BaseObjectSerializer:
# write detached or root objects to transports
if detached and self.write_transports:
serialized_data = ujson.dumps(object_builder)
for t in self.write_transports:
t.save_object(id=obj_id, serialized_object=ujson.dumps(object_builder))
t.save_object(id=obj_id, serialized_object=serialized_data)
del self.lineage[-1]
@@ -0,0 +1,32 @@
import pytest
from httpx import HTTPStatusError
from specklepy.core.api.connector_versions import (
ConnectorVersion,
ConnectorVersions,
get_connector_versions,
get_latest_version,
)
# NOTE: the tests in this file are testing against the live releases.speckle.dev server
# url defined in get_connector_versions.
def test_connector_versions():
res = get_connector_versions("blender")
assert isinstance(res, ConnectorVersions)
assert res.versions # Assuming the feed is not empty
def test_get_latest_version_throws_no_slug():
with pytest.raises(HTTPStatusError) as ex:
get_latest_version("non-existent-connector!", True)
assert "404" in str(ex.value)
def test_get_latest_version():
res = get_latest_version("blender", False)
assert isinstance(res, ConnectorVersion)