Skip to content

Commit 392214c

Browse files
authored
feat: add method to retrieve active bundle (#404)
Adds `active()` method to `Bundles` class to retrieve the currently active bundle. Resolves #380
1 parent 20de2bf commit 392214c

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import time
2+
from pathlib import Path
3+
4+
import pytest
5+
from packaging import version
6+
7+
from posit import connect
8+
9+
from . import CONNECT_VERSION
10+
11+
12+
class TestBundles:
13+
@classmethod
14+
def setup_class(cls):
15+
cls.client = connect.Client()
16+
cls.content = cls.client.content.create(
17+
name=f"test-bundles-{int(time.time())}",
18+
title="Test Bundles",
19+
access_type="all",
20+
)
21+
# Path to the test bundle
22+
bundle_path = Path(
23+
"../../../resources/connect/bundles/example-flask-minimal/bundle.tar.gz"
24+
)
25+
cls.bundle_path = (Path(__file__).parent / bundle_path).resolve()
26+
27+
@classmethod
28+
def teardown_class(cls):
29+
cls.content.delete()
30+
31+
def test_create_bundle(self):
32+
"""Test creating a bundle."""
33+
bundle = self.content.bundles.create(str(self.bundle_path))
34+
assert bundle["id"]
35+
assert bundle["content_guid"] == self.content["guid"]
36+
37+
def test_find_bundles(self):
38+
"""Test finding all bundles."""
39+
# Create a bundle first
40+
self.content.bundles.create(str(self.bundle_path))
41+
42+
# Find all bundles
43+
bundles = self.content.bundles.find()
44+
assert len(bundles) >= 1
45+
46+
def test_find_one_bundle(self):
47+
"""Test finding a single bundle."""
48+
# Create a bundle first
49+
self.content.bundles.create(str(self.bundle_path))
50+
51+
# Find one bundle
52+
bundle = self.content.bundles.find_one()
53+
assert bundle is not None
54+
assert bundle["content_guid"] == self.content["guid"]
55+
56+
def test_get_bundle(self):
57+
"""Test getting a specific bundle."""
58+
# Create a bundle first
59+
created_bundle = self.content.bundles.create(str(self.bundle_path))
60+
61+
# Get the bundle by ID
62+
bundle = self.content.bundles.get(created_bundle["id"])
63+
assert bundle["id"] == created_bundle["id"]
64+
65+
@pytest.mark.skipif(
66+
CONNECT_VERSION < version.parse("2025.02.0"), reason="Requires Connect 2025.02.0 or later"
67+
)
68+
def test_active_bundle(self):
69+
"""Test retrieving the active bundle."""
70+
# Initially, no bundle should be active
71+
assert self.content.bundles.active() is None
72+
73+
# Create and deploy a bundle
74+
bundle = self.content.bundles.create(str(self.bundle_path))
75+
task = bundle.deploy()
76+
task.wait_for()
77+
78+
# Wait for the bundle to become active
79+
max_retries = 10
80+
active_bundle = None
81+
for _ in range(max_retries):
82+
active_bundle = self.content.bundles.active()
83+
if active_bundle is not None:
84+
break
85+
time.sleep(1)
86+
87+
# Verify the bundle is now active
88+
assert active_bundle is not None
89+
assert active_bundle["id"] == bundle["id"]
90+
assert active_bundle.get("active") is True
91+
92+
# Create another bundle but don't deploy it
93+
bundle2 = self.content.bundles.create(str(self.bundle_path))
94+
95+
# Verify the active bundle is still the first one
96+
active_bundle = self.content.bundles.active()
97+
assert active_bundle is not None
98+
assert active_bundle["id"] == bundle["id"]
99+
assert active_bundle["id"] != bundle2["id"]

src/posit/connect/bundles.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from typing_extensions import TYPE_CHECKING, List
88

9+
from posit.connect.context import requires
10+
911
from . import resources, tasks
1012

1113
if TYPE_CHECKING:
@@ -193,6 +195,24 @@ def find_one(self) -> Bundle | None:
193195
bundles = self.find()
194196
return next(iter(bundles), None)
195197

198+
@requires("2025.02.0")
199+
def active(self) -> Bundle | None:
200+
"""Retrieve the active bundle.
201+
202+
Returns
203+
-------
204+
Bundle | None
205+
The currently active bundle, or None if no active bundle exists.
206+
207+
Examples
208+
--------
209+
>>> client = connect.Client()
210+
>>> content = client.content.get(guid)
211+
>>> bundle = content.bundles.active()
212+
"""
213+
bundles = self.find()
214+
return next((bundle for bundle in bundles if bundle.get("active", False)), None)
215+
196216
def get(self, uid: str) -> Bundle:
197217
"""Get a bundle.
198218

tests/posit/connect/test_bundles.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,66 @@ def test(self):
373373
assert mock_content_get.call_count == 1
374374
assert mock_bundle_get.call_count == 1
375375
assert bundle["id"] == bundle_id
376+
377+
378+
class TestBundlesActive:
379+
@responses.activate
380+
def test_active_bundle_exists(self):
381+
content_guid = "f2f37341-e21d-3d80-c698-a935ad614066"
382+
383+
# Create a modified copy of bundles.json with one bundle as active
384+
bundles_data = load_mock(f"v1/content/{content_guid}/bundles.json")
385+
active_bundles_data = bundles_data.copy()
386+
active_bundles_data[0]["active"] = True
387+
388+
# behavior
389+
mock_content_get = responses.get(
390+
f"https://connect.example/__api__/v1/content/{content_guid}",
391+
json=load_mock(f"v1/content/{content_guid}.json"),
392+
)
393+
394+
mock_bundles_get = responses.get(
395+
f"https://connect.example/__api__/v1/content/{content_guid}/bundles",
396+
json=active_bundles_data,
397+
)
398+
399+
# setup
400+
c = Client("https://connect.example", "12345")
401+
c._ctx.version = None
402+
403+
# invoke
404+
bundle = c.content.get(content_guid).bundles.active()
405+
406+
# assert
407+
assert mock_content_get.call_count == 1
408+
assert mock_bundles_get.call_count == 1
409+
assert bundle is not None
410+
assert bundle["id"] == "101"
411+
assert bundle["active"] is True
412+
413+
@responses.activate
414+
def test_no_active_bundle(self):
415+
content_guid = "f2f37341-e21d-3d80-c698-a935ad614066"
416+
417+
# behavior
418+
mock_content_get = responses.get(
419+
f"https://connect.example/__api__/v1/content/{content_guid}",
420+
json=load_mock(f"v1/content/{content_guid}.json"),
421+
)
422+
423+
mock_bundles_get = responses.get(
424+
f"https://connect.example/__api__/v1/content/{content_guid}/bundles",
425+
json=load_mock(f"v1/content/{content_guid}/bundles.json"),
426+
)
427+
428+
# setup
429+
c = Client("https://connect.example", "12345")
430+
c._ctx.version = None
431+
432+
# invoke
433+
bundle = c.content.get(content_guid).bundles.active()
434+
435+
# assert
436+
assert mock_content_get.call_count == 1
437+
assert mock_bundles_get.call_count == 1
438+
assert bundle is None

0 commit comments

Comments
 (0)