Skip to content

Commit b06ef76

Browse files
authored
Merge branch 'main' into schloerke/345-tags
2 parents b98edf2 + 918bd5f commit b06ef76

File tree

23 files changed

+1013
-217
lines changed

23 files changed

+1013
-217
lines changed

integration/tests/posit/connect/test_content_item_permissions.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
17
from posit import connect
2-
from posit.connect.content import ContentItem
8+
9+
if TYPE_CHECKING:
10+
from posit.connect.content import ContentItem
11+
from posit.connect.permissions import Permission
312

413

514
class TestContentPermissions:
@@ -48,7 +57,7 @@ def test_permissions_add_destroy(self):
4857
role="owner",
4958
)
5059

51-
def assert_permissions_match_guids(permissions, objs_with_guid):
60+
def assert_permissions_match_guids(permissions: list[Permission], objs_with_guid):
5261
for permission, obj_with_guid in zip(permissions, objs_with_guid):
5362
assert permission["principal_guid"] == obj_with_guid["guid"]
5463

@@ -59,11 +68,9 @@ def assert_permissions_match_guids(permissions, objs_with_guid):
5968
)
6069

6170
# Remove permissions (and from some that isn't an owner)
62-
destroyed_permissions = self.content.permissions.destroy(self.user_aron, self.user_bill)
63-
assert_permissions_match_guids(
64-
destroyed_permissions,
65-
[self.user_aron],
66-
)
71+
self.content.permissions.destroy(self.user_aron)
72+
with pytest.raises(ValueError):
73+
self.content.permissions.destroy(self.user_bill)
6774

6875
# Prove they have been removed
6976
assert_permissions_match_guids(
@@ -72,11 +79,7 @@ def assert_permissions_match_guids(permissions, objs_with_guid):
7279
)
7380

7481
# Remove the last permission
75-
destroyed_permissions = self.content.permissions.destroy(self.group_friends)
76-
assert_permissions_match_guids(
77-
destroyed_permissions,
78-
[self.group_friends],
79-
)
82+
self.content.permissions.destroy(self.group_friends)
8083

8184
# Prove they have been removed
8285
assert self.content.permissions.find() == []

integration/tests/posit/connect/test_users.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,48 @@ def test_get(self):
5151
assert self.client.users.get(self.bill["guid"]) == self.bill
5252
assert self.client.users.get(self.cole["guid"]) == self.cole
5353

54+
# Also tests Groups.members
55+
def test_user_group_interactions(self):
56+
try:
57+
test_group = self.client.groups.create(name="UnitFriends")
58+
59+
# `Group.members.count()`
60+
assert test_group.members.count() == 0
61+
62+
# `Group.members.add()`
63+
test_group.members.add(self.bill)
64+
# `User.groups.add()`
65+
assert test_group.members.count() == 1
66+
self.cole.groups.add(test_group)
67+
assert test_group.members.count() == 2
68+
69+
# `Group.members.find()`
70+
group_users = test_group.members.find()
71+
assert len(group_users) == 2
72+
assert group_users[0]["guid"] == self.bill["guid"]
73+
assert group_users[1]["guid"] == self.cole["guid"]
74+
75+
# `User.group.find()`
76+
bill_groups = self.bill.groups.find()
77+
assert len(bill_groups) == 1
78+
assert bill_groups[0]["guid"] == test_group["guid"]
79+
80+
# `Group.members.delete()`
81+
test_group.members.delete(self.bill)
82+
assert test_group.members.count() == 1
83+
84+
# `User.groups.delete()`
85+
self.cole.groups.delete(test_group)
86+
assert test_group.members.count() == 0
87+
88+
finally:
89+
groups = self.client.groups.find(prefix="UnitFriends")
90+
if len(groups) > 0:
91+
test_group = groups[0]
92+
test_group.delete()
93+
94+
assert len(self.client.groups.find(prefix="UnitFriends")) == 0
95+
5496

5597
class TestUserContent:
5698
"""Checks behavior of the content attribute."""

src/posit/connect/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def __init__(self, *args, **kwargs) -> None:
158158
session.hooks["response"].append(hooks.handle_errors)
159159
self.session = session
160160
self.resource_params = ResourceParameters(session, self.cfg.url)
161-
self._ctx = Context(self.session, self.cfg.url)
161+
self._ctx = Context(self)
162162

163163
@property
164164
def version(self) -> str | None:
@@ -182,7 +182,7 @@ def me(self) -> User:
182182
User
183183
The currently authenticated user.
184184
"""
185-
return me.get(self.resource_params)
185+
return me.get(self._ctx)
186186

187187
@property
188188
def groups(self) -> Groups:
@@ -193,7 +193,7 @@ def groups(self) -> Groups:
193193
Groups
194194
The groups resource interface.
195195
"""
196-
return Groups(self.resource_params)
196+
return Groups(self._ctx)
197197

198198
@property
199199
def tasks(self) -> Tasks:
@@ -217,7 +217,7 @@ def users(self) -> Users:
217217
Users
218218
The users resource instance.
219219
"""
220-
return Users(self.resource_params)
220+
return Users(self._ctx)
221221

222222
@property
223223
def content(self) -> Content:
@@ -229,7 +229,7 @@ def content(self) -> Content:
229229
Content
230230
The content resource instance.
231231
"""
232-
return Content(self.resource_params)
232+
return Content(self._ctx)
233233

234234
@property
235235
def tags(self) -> Tags:

src/posit/connect/content.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from . import tasks
2121
from ._api import ApiDictEndpoint, JsonifiableDict
2222
from .bundles import Bundles
23-
from .context import Context
2423
from .env import EnvVars
2524
from .errors import ClientError
2625
from .jobs import JobsMixin
@@ -33,6 +32,7 @@
3332
from .variants import Variants
3433

3534
if TYPE_CHECKING:
35+
from .context import Context
3636
from .tasks import Task
3737

3838

@@ -222,30 +222,32 @@ class _AttrsCreate(_AttrsBase):
222222
@overload
223223
def __init__(
224224
self,
225+
ctx: Context,
225226
/,
226-
params: ResourceParameters,
227+
*,
227228
guid: str,
228229
) -> None: ...
229230

230231
@overload
231232
def __init__(
232233
self,
234+
ctx: Context,
233235
/,
234-
params: ResourceParameters,
236+
*,
235237
guid: str,
236238
**kwargs: Unpack[ContentItem._Attrs],
237239
) -> None: ...
238240

239241
def __init__(
240242
self,
243+
ctx: Context,
241244
/,
242-
params: ResourceParameters,
245+
*,
243246
guid: str,
244247
**kwargs: Unpack[ContentItem._AttrsNotRequired],
245248
) -> None:
246249
_assert_guid(guid)
247250

248-
ctx = Context(params.session, params.url)
249251
path = f"v1/content/{guid}"
250252
super().__init__(ctx, path, guid=guid, **kwargs)
251253

@@ -292,8 +294,8 @@ def create_repository(
292294
def delete(self) -> None:
293295
"""Delete the content item."""
294296
path = f"v1/content/{self['guid']}"
295-
url = self.params.url + path
296-
self.params.session.delete(url)
297+
url = self._ctx.url + path
298+
self._ctx.session.delete(url)
297299

298300
def deploy(self) -> tasks.Task:
299301
"""Deploy the content.
@@ -312,8 +314,8 @@ def deploy(self) -> tasks.Task:
312314
None
313315
"""
314316
path = f"v1/content/{self['guid']}/deploy"
315-
url = self.params.url + path
316-
response = self.params.session.post(url, json={"bundle_id": None})
317+
url = self._ctx.url + path
318+
response = self._ctx.session.post(url, json={"bundle_id": None})
317319
result = response.json()
318320
ts = tasks.Tasks(self.params)
319321
return ts.get(result["task_id"])
@@ -368,8 +370,8 @@ def restart(self) -> None:
368370
self.environment_variables.create(key, unix_epoch_in_seconds)
369371
self.environment_variables.delete(key)
370372
# GET via the base Connect URL to force create a new worker thread.
371-
url = posixpath.join(dirname(self.params.url), f"content/{self['guid']}")
372-
self.params.session.get(url)
373+
url = posixpath.join(dirname(self._ctx.url), f"content/{self['guid']}")
374+
self._ctx.session.get(url)
373375
return None
374376
else:
375377
raise ValueError(
@@ -439,8 +441,8 @@ def update(
439441
-------
440442
None
441443
"""
442-
url = self.params.url + f"v1/content/{self['guid']}"
443-
response = self.params.session.patch(url, json=attrs)
444+
url = self._ctx.url + f"v1/content/{self['guid']}"
445+
response = self._ctx.session.patch(url, json=attrs)
444446
super().update(**response.json())
445447

446448
# Relationships
@@ -465,7 +467,9 @@ def owner(self) -> dict:
465467
# If it's not included, we can retrieve the information by `owner_guid`
466468
from .users import Users
467469

468-
self["owner"] = Users(self.params).get(self["owner_guid"])
470+
self["owner"] = Users(
471+
self._ctx,
472+
).get(self["owner_guid"])
469473
return self["owner"]
470474

471475
@property
@@ -523,12 +527,13 @@ class Content(Resources):
523527

524528
def __init__(
525529
self,
526-
params: ResourceParameters,
530+
ctx: Context,
527531
*,
528532
owner_guid: str | None = None,
529533
) -> None:
530-
super().__init__(params)
534+
super().__init__(ctx.client.resource_params)
531535
self.owner_guid = owner_guid
536+
self._ctx = ctx
532537

533538
def count(self) -> int:
534539
"""Count the number of content items.
@@ -603,9 +608,9 @@ def create(
603608
ContentItem
604609
"""
605610
path = "v1/content"
606-
url = self.params.url + path
607-
response = self.params.session.post(url, json=attrs)
608-
return ContentItem(self.params, **response.json())
611+
url = self._ctx.url + path
612+
response = self._ctx.session.post(url, json=attrs)
613+
return ContentItem(self._ctx, **response.json())
609614

610615
@overload
611616
def find(
@@ -691,11 +696,11 @@ def find(self, include: Optional[str | list[Any]] = None, **conditions) -> List[
691696
conditions["owner_guid"] = self.owner_guid
692697

693698
path = "v1/content"
694-
url = self.params.url + path
695-
response = self.params.session.get(url, params=conditions)
699+
url = self._ctx.url + path
700+
response = self._ctx.session.get(url, params=conditions)
696701
return [
697702
ContentItem(
698-
self.params,
703+
self._ctx,
699704
**result,
700705
)
701706
for result in response.json()
@@ -864,6 +869,6 @@ def get(self, guid: str) -> ContentItem:
864869
ContentItem
865870
"""
866871
path = f"v1/content/{guid}"
867-
url = self.params.url + path
868-
response = self.params.session.get(url)
869-
return ContentItem(self.params, **response.json())
872+
url = self._ctx.url + path
873+
response = self._ctx.session.get(url)
874+
return ContentItem(self._ctx, **response.json())

src/posit/connect/context.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from __future__ import annotations
22

33
import functools
4+
import weakref
45
from typing import TYPE_CHECKING, Protocol
56

67
from packaging.version import Version
78

89
if TYPE_CHECKING:
910
import requests
1011

12+
from .client import Client
1113
from .urls import Url
1214

1315

@@ -28,9 +30,12 @@ def wrapper(instance: ContextManager, *args, **kwargs):
2830

2931

3032
class Context:
31-
def __init__(self, session: requests.Session, url: Url):
32-
self.session = session
33-
self.url = url
33+
def __init__(self, client: Client):
34+
self.session: requests.Session = client.session
35+
self.url: Url = client.cfg.url
36+
# Since this is a child object of the client, we use a weak reference to avoid circular
37+
# references (which would prevent garbage collection)
38+
self.client: Client = weakref.proxy(client)
3439

3540
@property
3641
def version(self) -> str | None:

0 commit comments

Comments
 (0)