Skip to content

Commit 3bcdf72

Browse files
feat (api): projects with permissions (#430)
* adds project with permissions * removes the project resource with permissions * fix the tests
1 parent adc1105 commit 3bcdf72

File tree

10 files changed

+324
-6
lines changed

10 files changed

+324
-6
lines changed

src/specklepy/api/resources/current/active_user_resource.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
ResourceCollection,
1212
User,
1313
)
14-
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
14+
from specklepy.core.api.models.current import (
15+
PermissionCheckResult,
16+
ProjectWithPermissions,
17+
Workspace,
18+
)
1519
from specklepy.core.api.resources import ActiveUserResource as CoreResource
1620
from specklepy.logging import metrics
1721

@@ -51,6 +55,22 @@ def get_projects(
5155
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Projects"})
5256
return super().get_projects(limit=limit, cursor=cursor, filter=filter)
5357

58+
def get_projects_with_permissions(
59+
self,
60+
*,
61+
limit: int = 25,
62+
cursor: Optional[str] = None,
63+
filter: Optional[UserProjectsFilter] = None,
64+
) -> ResourceCollection[ProjectWithPermissions]:
65+
metrics.track(
66+
metrics.SDK,
67+
self.account,
68+
{"name": "Active User Get Projects With Permissions"},
69+
)
70+
return super().get_projects_with_permissions(
71+
limit=limit, cursor=cursor, filter=filter
72+
)
73+
5474
def get_project_invites(self) -> List[PendingStreamCollaborator]:
5575
metrics.track(
5676
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}

src/specklepy/api/resources/current/project_resource.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
ProjectUpdateRoleInput,
88
WorkspaceProjectCreateInput,
99
)
10-
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
10+
from specklepy.core.api.models import (
11+
Project,
12+
ProjectWithModels,
13+
ProjectWithTeam,
14+
)
1115
from specklepy.core.api.models.current import ProjectPermissionChecks
1216
from specklepy.core.api.resources import ProjectResource as CoreResource
1317
from specklepy.logging import metrics

src/specklepy/api/resources/current/workspace_resource.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from typing import Optional
22

33
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
4-
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
4+
from specklepy.core.api.models.current import (
5+
Project,
6+
ProjectWithPermissions,
7+
ResourceCollection,
8+
Workspace,
9+
)
510
from specklepy.core.api.resources import WorkspaceResource as CoreResource
611
from specklepy.logging import metrics
712

@@ -30,3 +35,19 @@ def get_projects(
3035
) -> ResourceCollection[Project]:
3136
metrics.track(metrics.SDK, self.account, {"name": "Workspace Get Projects"})
3237
return super().get_projects(workspace_id, limit, cursor, filter)
38+
39+
def get_projects_with_permissions(
40+
self,
41+
workspace_id: str,
42+
limit: int = 25,
43+
cursor: Optional[str] = None,
44+
filter: Optional[WorksaceProjectsFilter] = None,
45+
) -> ResourceCollection[ProjectWithPermissions]:
46+
metrics.track(
47+
metrics.SDK,
48+
self.account,
49+
{"name": "Workspace Get Projects With Permissions"},
50+
)
51+
return super().get_projects_with_permissions(
52+
workspace_id, limit, cursor, filter
53+
)

src/specklepy/core/api/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
ProjectCollaborator,
99
ProjectCommentCollection,
1010
ProjectWithModels,
11+
ProjectWithPermissions,
1112
ProjectWithTeam,
1213
ResourceCollection,
1314
ServerConfiguration,
@@ -39,6 +40,7 @@
3940
"ModelWithVersions",
4041
"Project",
4142
"ProjectWithModels",
43+
"ProjectWithPermissions",
4244
"ProjectWithTeam",
4345
"ProjectCommentCollection",
4446
"UserSearchResultCollection",

src/specklepy/core/api/models/current.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ class ProjectWithModels(Project):
176176
models: ResourceCollection[Model]
177177

178178

179+
class ProjectWithPermissions(Project):
180+
permissions: ProjectPermissionChecks
181+
182+
179183
class ProjectWithTeam(Project):
180184
invited_team: List[PendingStreamCollaborator]
181185
team: List[ProjectCollaborator]

src/specklepy/core/api/resources/current/active_user_resource.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
ResourceCollection,
1414
User,
1515
)
16-
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
16+
from specklepy.core.api.models.current import (
17+
PermissionCheckResult,
18+
ProjectWithPermissions,
19+
Workspace,
20+
)
1721
from specklepy.core.api.resource import ResourceBase
1822
from specklepy.core.api.responses import DataResponse
1923
from specklepy.logging.exceptions import GraphQLException
@@ -338,3 +342,84 @@ def get_active_workspace(self) -> Optional[Workspace]:
338342
)
339343

340344
return response.data.data
345+
346+
def get_projects_with_permissions(
347+
self,
348+
*,
349+
limit: int = 25,
350+
cursor: Optional[str] = None,
351+
filter: Optional[UserProjectsFilter] = None,
352+
) -> ResourceCollection[ProjectWithPermissions]:
353+
"""
354+
Gets the currently active user's projects with their permissions.
355+
This is useful for checking what actions can be performed on each project.
356+
"""
357+
QUERY = gql(
358+
"""
359+
query User($limit : Int!, $cursor: String, $filter: UserProjectsFilter) {
360+
data:activeUser {
361+
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
362+
totalCount
363+
cursor
364+
items {
365+
id
366+
name
367+
description
368+
visibility
369+
allowPublicComments
370+
role
371+
createdAt
372+
updatedAt
373+
sourceApps
374+
workspaceId
375+
permissions {
376+
canCreateModel {
377+
code
378+
authorized
379+
message
380+
}
381+
canDelete {
382+
code
383+
authorized
384+
message
385+
}
386+
canLoad {
387+
code
388+
authorized
389+
message
390+
}
391+
canPublish {
392+
code
393+
authorized
394+
message
395+
}
396+
}
397+
}
398+
}
399+
}
400+
}
401+
"""
402+
)
403+
404+
variables = {
405+
"limit": limit,
406+
"cursor": cursor,
407+
"filter": filter.model_dump(warnings="error", by_alias=True)
408+
if filter
409+
else None,
410+
}
411+
412+
response = self.make_request_and_parse_response(
413+
DataResponse[
414+
Optional[DataResponse[ResourceCollection[ProjectWithPermissions]]]
415+
],
416+
QUERY,
417+
variables,
418+
)
419+
420+
if response.data is None:
421+
raise GraphQLException(
422+
"GraphQL response indicated that the ActiveUser could not be found"
423+
)
424+
425+
return response.data.data

src/specklepy/core/api/resources/current/project_resource.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
ProjectUpdateRoleInput,
1010
WorkspaceProjectCreateInput,
1111
)
12-
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
12+
from specklepy.core.api.models import (
13+
Project,
14+
ProjectWithModels,
15+
ProjectWithTeam,
16+
)
1317
from specklepy.core.api.models.current import ProjectPermissionChecks
1418
from specklepy.core.api.resource import ResourceBase
1519
from specklepy.core.api.responses import DataResponse

src/specklepy/core/api/resources/current/workspace_resource.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from gql import gql
44

55
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
6-
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
6+
from specklepy.core.api.models.current import (
7+
Project,
8+
ProjectWithPermissions,
9+
ResourceCollection,
10+
Workspace,
11+
)
712
from specklepy.core.api.resource import ResourceBase
813
from specklepy.core.api.responses import DataResponse
914

@@ -104,3 +109,72 @@ def get_projects(
104109
return self.make_request_and_parse_response(
105110
DataResponse[DataResponse[ResourceCollection[Project]]], QUERY, variables
106111
).data.data
112+
113+
def get_projects_with_permissions(
114+
self,
115+
workspace_id: str,
116+
limit: int = 25,
117+
cursor: Optional[str] = None,
118+
filter: Optional[WorksaceProjectsFilter] = None,
119+
) -> ResourceCollection[ProjectWithPermissions]:
120+
QUERY = gql(
121+
"""
122+
query Workspace($workspaceId: String!, $limit: Int!, $cursor: String, $filter: WorkspaceProjectsFilter) {
123+
data:workspace(id: $workspaceId) {
124+
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
125+
cursor
126+
items {
127+
allowPublicComments
128+
createdAt
129+
description
130+
id
131+
name
132+
role
133+
sourceApps
134+
updatedAt
135+
visibility
136+
workspaceId
137+
permissions {
138+
canCreateModel {
139+
code
140+
authorized
141+
message
142+
}
143+
canDelete {
144+
code
145+
authorized
146+
message
147+
}
148+
canLoad {
149+
code
150+
authorized
151+
message
152+
}
153+
canPublish {
154+
code
155+
authorized
156+
message
157+
}
158+
}
159+
}
160+
totalCount
161+
}
162+
}
163+
}
164+
""" # noqa: E501
165+
)
166+
167+
variables = {
168+
"workspaceId": workspace_id,
169+
"limit": limit,
170+
"cursor": cursor,
171+
"filter": filter.model_dump(warnings="error", by_alias=True)
172+
if filter
173+
else None,
174+
}
175+
176+
return self.make_request_and_parse_response(
177+
DataResponse[DataResponse[ResourceCollection[ProjectWithPermissions]]],
178+
QUERY,
179+
variables,
180+
).data.data
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pytest
2+
3+
from specklepy.api.client import SpeckleClient
4+
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
5+
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
6+
from specklepy.core.api.models.current import (
7+
Project,
8+
ProjectWithPermissions,
9+
ResourceCollection,
10+
)
11+
12+
13+
@pytest.mark.run()
14+
class TestActiveUserResourcePermissions:
15+
@pytest.fixture()
16+
def test_project(self, client: SpeckleClient) -> Project:
17+
project = client.project.create(
18+
ProjectCreateInput(
19+
name="test project for active user permissions",
20+
description="test description",
21+
visibility=None,
22+
)
23+
)
24+
return project
25+
26+
def test_active_user_get_projects_with_permissions(
27+
self, client: SpeckleClient, test_project: Project
28+
):
29+
result = client.active_user.get_projects_with_permissions()
30+
31+
assert isinstance(result, ResourceCollection)
32+
assert len(result.items) >= 1
33+
34+
test_project_with_permissions = None
35+
for project in result.items:
36+
if project.id == test_project.id:
37+
test_project_with_permissions = project
38+
break
39+
40+
assert test_project_with_permissions is not None
41+
assert isinstance(test_project_with_permissions, ProjectWithPermissions)
42+
43+
assert hasattr(test_project_with_permissions, "permissions")
44+
assert test_project_with_permissions.permissions is not None
45+
46+
assert test_project_with_permissions.id == test_project.id
47+
assert test_project_with_permissions.name == test_project.name
48+
49+
permissions = test_project_with_permissions.permissions
50+
assert hasattr(permissions, "can_create_model")
51+
assert hasattr(permissions, "can_delete")
52+
assert hasattr(permissions, "can_load")
53+
assert hasattr(permissions, "can_publish")
54+
55+
assert permissions.can_create_model.authorized is True
56+
assert permissions.can_delete.authorized is True
57+
assert permissions.can_load.authorized is True
58+
assert permissions.can_publish.authorized is True
59+
60+
def test_active_user_get_projects_with_permissions_with_filter(
61+
self, client: SpeckleClient, test_project: Project
62+
):
63+
"""test getting active user's projects with permissions using a filter."""
64+
filter = UserProjectsFilter(search=test_project.name)
65+
66+
result = client.active_user.get_projects_with_permissions(filter=filter)
67+
68+
assert isinstance(result, ResourceCollection)
69+
assert len(result.items) >= 1
70+
assert result.total_count >= 1
71+
72+
project_with_permissions = result.items[0]
73+
assert isinstance(project_with_permissions, ProjectWithPermissions)
74+
assert project_with_permissions.id == test_project.id
75+
76+
assert hasattr(project_with_permissions, "permissions")
77+
assert project_with_permissions.permissions is not None
78+
79+
def test_active_user_projects_with_permissions_method_exists(
80+
self, client: SpeckleClient
81+
):
82+
"""test that the method exists and is callable on active user resource."""
83+
assert hasattr(client.active_user, "get_projects_with_permissions")
84+
method = client.active_user.get_projects_with_permissions
85+
assert callable(method)

0 commit comments

Comments
 (0)