Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add/test many tag, tags, and content item tags methods #346

Merged
merged 23 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1cce667
Add/test many tag, tags, and content item tags methods
schloerke Nov 26, 2024
061c667
Remove unused codes
schloerke Nov 26, 2024
0ca43a6
Add missing tests
schloerke Nov 27, 2024
389dd24
Remove json when deleting tag
schloerke Dec 2, 2024
7c1dfce
Merge branch 'main' into schloerke/345-tags
schloerke Dec 3, 2024
01471e9
Separate out `parent=` into `parent=` or `parent_id=`. Use overloads …
schloerke Dec 3, 2024
b98edf2
Clean up integration test
schloerke Dec 3, 2024
b06ef76
Merge branch 'main' into schloerke/345-tags
schloerke Dec 5, 2024
ac39b1d
Merge branch 'main' into schloerke/345-tags
schloerke Dec 5, 2024
b82ca52
Resolve TODOs
schloerke Dec 5, 2024
b4f418e
Update algo to remove tags with no parents for faster second pass
schloerke Dec 5, 2024
df626d2
Apply suggestions from code review
schloerke Dec 5, 2024
6f8fb46
Name changes
schloerke Dec 5, 2024
267f58f
Add content_items to child / descendant tags
schloerke Dec 6, 2024
2ad2b3a
Update test_tags.py
schloerke Dec 6, 2024
6b56243
Combine classes and have ABC methods to implement; Add many examples
schloerke Dec 6, 2024
e5cd39c
Make methods singular
schloerke Dec 6, 2024
110eae6
Add Examples
schloerke Dec 6, 2024
ef0487a
Update permissions.py
schloerke Dec 6, 2024
523550a
Be sure the content items are properties
schloerke Dec 6, 2024
e753556
Test content item properties
schloerke Dec 6, 2024
ef4bc00
Update integration tests. Test more of tag content items
schloerke Dec 6, 2024
d705d92
Remove `.content_items` property from `ChildTags` and `DescendantTags…
schloerke Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions integration/tests/posit/connect/test_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from typing import TYPE_CHECKING

from posit import connect

if TYPE_CHECKING:
from posit.connect.content import ContentItem


# add integration tests here!
class TestTags:
@classmethod
def setup_class(cls):
cls.client = connect.Client()
cls.contentA = cls.client.content.create(name="Content_A")
cls.contentB = cls.client.content.create(name="Content_B")
cls.contentC = cls.client.content.create(name="Content_C")

@classmethod
def teardown_class(cls):
assert len(cls.client.tags.find()) == 0
cls.contentA.delete()
cls.contentB.delete()
cls.contentC.delete()
assert len(cls.client.content.find()) == 0

def test_tags_find(self):
tags = self.client.tags.find()
assert len(tags) == 0

def test_tags_create_destroy(self):
tagA = self.client.tags.create(name="tagA")
tagB = self.client.tags.create(name="tagB")
tagC = self.client.tags.create(name="tagC", parent=tagA)

assert len(self.client.tags.find()) == 3

tagA.destroy()
tagB.destroy()
## Deleted when tag A is deleted
# tagC.destroy()

assert len(self.client.tags.find()) == 0

def test_tag_descendants(self):
tagA = self.client.tags.create(name="tagA")
tagB = self.client.tags.create(name="tagB")
tagC = self.client.tags.create(name="tagC", parent=tagA)
tagD = self.client.tags.create(name="tagD", parent=tagC)

assert tagA.descendant_tags.find() == [tagC, tagD]

assert len(tagB.descendant_tags.find()) == 0
assert tagC.descendant_tags.find() == [tagD]

# cleanup
tagA.destroy()
tagB.destroy()
assert len(self.client.tags.find()) == 0

def test_tag_children(self):
tagA = self.client.tags.create(name="tagA_children")
tagB = self.client.tags.create(name="tagB_children")
tagC = self.client.tags.create(name="tagC_children", parent=tagA)
tagD = self.client.tags.create(name="tagD_children", parent=tagC)

assert tagA.children_tags.find() == [tagC]
assert tagB.children_tags.find() == []
assert tagC.children_tags.find() == [tagD]

# cleanup
tagA.destroy()
tagB.destroy()
assert len(self.client.tags.find()) == 0

def test_tag_parent(self):
tagA = self.client.tags.create(name="tagA_parent")
tagB = self.client.tags.create(name="tagB_parent")
tagC = self.client.tags.create(name="tagC_parent", parent=tagA)
tagD = self.client.tags.create(name="tagD_parent", parent=tagC)

assert tagA.parent_tag is None
assert tagB.parent_tag is None
assert tagC.parent_tag == tagA
assert tagD.parent_tag == tagC

# cleanup
tagA.destroy()
tagB.destroy()
assert len(self.client.tags.find()) == 0

def test_content_item_tags(self):
tagRoot = self.client.tags.create(name="tagRoot_content_item_tags")
tagA = self.client.tags.create(name="tagA_content_item_tags", parent=tagRoot)
tagB = self.client.tags.create(name="tagB_content_item_tags", parent=tagRoot)
tagC = self.client.tags.create(name="tagC_content_item_tags", parent=tagA)
tagD = self.client.tags.create(name="tagD_content_item_tags", parent=tagC)

assert len(self.contentA.tags.find()) == 0

self.contentA.tags.add(tagD)
self.contentA.tags.add(tagB)

# tagB, tagD, tagC (parent of tagD), tagA (parent of tagC)
# tagD + tagC
# tagRoot is considered a "category" and is "not a tag"
assert len(self.contentA.tags.find()) == 4

# Removes tagB
self.contentA.tags.delete(tagB)
assert len(self.contentA.tags.find()) == 4 - 1

# Removes tagC and tagD (parent of tagC)
self.contentA.tags.delete(tagC)
assert len(self.contentA.tags.find()) == 4 - 1 - 2

# cleanup
tagRoot.destroy()
assert len(self.client.tags.find()) == 0

def test_tag_content_items(self):
tagRoot = self.client.tags.create(name="tagRoot_tag_content_items")
tagA = self.client.tags.create(name="tagA_tag_content_items", parent=tagRoot)
tagB = self.client.tags.create(name="tagB_tag_content_items", parent=tagRoot)
tagC = self.client.tags.create(name="tagC_tag_content_items", parent=tagA)
tagD = self.client.tags.create(name="tagD_tag_content_items", parent=tagC)

assert len(tagA.content_items.find()) == 0
assert len(tagB.content_items.find()) == 0
assert len(tagC.content_items.find()) == 0
assert len(tagD.content_items.find()) == 0

self.contentA.tags.add(tagD)
self.contentA.tags.add(tagB)

self.contentB.tags.add(tagA)
self.contentB.tags.add(tagC)

self.contentC.tags.add(tagC)

assert len(self.contentA.tags.find()) == 4
assert len(self.contentB.tags.find()) == 2
assert len(self.contentC.tags.find()) == 2

assert len(tagA.content_items.find()) == 3
assert len(tagB.content_items.find()) == 1
assert len(tagC.content_items.find()) == 3

# Make sure unique content items are found
content_items_list: list[ContentItem] = []
for tag in [tagA, *tagA.descendant_tags.find()]:
content_items_list.extend(tag.content_items.find())
# Get unique content_item guids
content_item_guids = set[str]()
for content_item in content_items_list:
if content_item["guid"] not in content_item_guids:
content_item_guids.add(content_item["guid"])

assert content_item_guids == {
self.contentA["guid"],
self.contentB["guid"],
self.contentC["guid"],
}

self.contentA.tags.delete(tagRoot)
self.contentB.tags.delete(tagRoot)
self.contentC.tags.delete(tagRoot)

assert len(tagA.content_items.find()) == 0
assert len(tagB.content_items.find()) == 0
assert len(tagC.content_items.find()) == 0

# cleanup
tagRoot.destroy()
assert len(self.client.tags.find()) == 0
24 changes: 24 additions & 0 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from requests import Response, Session

from posit.connect.tags import Tags

from . import hooks, me
from .auth import Auth
from .config import Config
Expand Down Expand Up @@ -229,6 +231,28 @@ def content(self) -> Content:
"""
return Content(self.resource_params)

@property
def tags(self) -> Tags:
"""
The tags resource interface.

Returns
-------
Tags
The tags resource instance.

Examples
--------
```python
import posit

client = posit.connect.Client()

tags = client.tags.find()
```
"""
return Tags(self._ctx, "v1/tags")

@property
def metrics(self) -> Metrics:
"""
Expand Down
11 changes: 11 additions & 0 deletions src/posit/connect/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .packages import ContentPackagesMixin as PackagesMixin
from .permissions import Permissions
from .resources import Resource, ResourceParameters, Resources
from .tags import ContentItemTags
from .vanities import VanityMixin
from .variants import Variants

Expand Down Expand Up @@ -496,6 +497,16 @@ def is_rendered(self) -> bool:
"quarto-static",
}

@property
def tags(self) -> ContentItemTags:
path = f"v1/content/{self['guid']}/tags"
return ContentItemTags(
self._ctx,
path,
tags_path="v1/tags",
content_guid=self["guid"],
)


class Content(Resources):
"""Content resource.
Expand Down
2 changes: 1 addition & 1 deletion src/posit/connect/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def destroy(self, *permissions: str | Group | User | Permission) -> list[Permiss
Returns
-------
list[Permission]
The removed permissions. If a permission is not found, there is nothing to remove and
The removed permissions. If a permission is not found, there is nothing to remove and
it is not included in the returned list.

Examples
Expand Down
Loading
Loading