Skip to content

Commit d6c1249

Browse files
committed
feat: add version check support
1 parent 0614ae4 commit d6c1249

File tree

9 files changed

+168
-14
lines changed

9 files changed

+168
-14
lines changed

src/posit/connect/client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
from __future__ import annotations
44

5-
from typing import overload
5+
from typing import Optional, overload
66

77
from requests import Response, Session
88

99
from . import hooks, me
1010
from .auth import Auth
1111
from .config import Config
1212
from .content import Content
13+
from .context import Context, ContextManager, requires
1314
from .groups import Groups
1415
from .metrics import Metrics
1516
from .oauth import OAuth
@@ -18,7 +19,7 @@
1819
from .users import User, Users
1920

2021

21-
class Client:
22+
class Client(ContextManager):
2223
"""
2324
Client connection for Posit Connect.
2425
@@ -156,9 +157,10 @@ def __init__(self, *args, **kwargs) -> None:
156157
session.hooks["response"].append(hooks.handle_errors)
157158
self.session = session
158159
self.resource_params = ResourceParameters(session, self.cfg.url)
160+
self.ctx = Context(self.session, self.cfg.url)
159161

160162
@property
161-
def version(self) -> str:
163+
def version(self) -> Optional[str]:
162164
"""
163165
The server version.
164166
@@ -167,7 +169,7 @@ def version(self) -> str:
167169
str
168170
The version of the Posit Connect server.
169171
"""
170-
return self.get("server_settings").json()["version"]
172+
return self.ctx.version
171173

172174
@property
173175
def me(self) -> User:
@@ -257,6 +259,7 @@ def metrics(self) -> Metrics:
257259
return Metrics(self.resource_params)
258260

259261
@property
262+
@requires(version="2024.08.0")
260263
def oauth(self) -> OAuth:
261264
"""
262265
The OAuth API interface.

src/posit/connect/context.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import functools
2+
from typing import Optional, Protocol
3+
4+
5+
def requires(version: str):
6+
def decorator(func):
7+
@functools.wraps(func)
8+
def wrapper(instance: ContextManager, *args, **kwargs):
9+
ctx = instance.ctx
10+
if ctx.version and ctx.version < version:
11+
raise RuntimeError(
12+
f"This API is not available in Connect version {ctx.version}. Please upgrade to version {version} or later.",
13+
)
14+
return func(instance, *args, **kwargs)
15+
16+
return wrapper
17+
18+
return decorator
19+
20+
21+
class Context(dict):
22+
def __init__(self, session, url):
23+
self.session = session
24+
self.url = url
25+
26+
@property
27+
def version(self) -> Optional[str]:
28+
try:
29+
value = self["version"]
30+
except KeyError:
31+
endpoint = self.url + "server_settings"
32+
response = self.session.get(endpoint)
33+
result = response.json()
34+
value = self["version"] = result.get("version")
35+
return value
36+
37+
@version.setter
38+
def version(self, value: str):
39+
self["version"] = value
40+
41+
42+
class ContextManager(Protocol):
43+
ctx: Context

tests/posit/connect/external/test_databricks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def test_posit_credentials_provider(self):
4848
register_mocks()
4949

5050
client = Client(api_key="12345", url="https://connect.example/")
51+
client.ctx.version = "2024.08.0"
5152
cp = PositCredentialsProvider(client=client, user_session_token="cit")
5253
assert cp() == {"Authorization": f"Bearer dynamic-viewer-access-token"}
5354

@@ -57,6 +58,7 @@ def test_posit_credentials_strategy(self):
5758
register_mocks()
5859

5960
client = Client(api_key="12345", url="https://connect.example/")
61+
client.ctx.version = "2024.08.0"
6062
cs = PositCredentialsStrategy(
6163
local_strategy=mock_strategy(),
6264
user_session_token="cit",

tests/posit/connect/external/test_snowflake.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_posit_authenticator(self):
3333
register_mocks()
3434

3535
client = Client(api_key="12345", url="https://connect.example/")
36+
client.ctx.version = "2024.08.0"
3637
auth = PositAuthenticator(
3738
local_authenticator="SNOWFLAKE",
3839
user_session_token="cit",
@@ -44,6 +45,7 @@ def test_posit_authenticator(self):
4445
def test_posit_authenticator_fallback(self):
4546
# local_authenticator is used when the content is running locally
4647
client = Client(api_key="12345", url="https://connect.example/")
48+
client.ctx.version = "2024.08.0"
4749
auth = PositAuthenticator(
4850
local_authenticator="SNOWFLAKE",
4951
user_session_token="cit",

tests/posit/connect/oauth/test_associations.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test(self):
5555

5656
# setup
5757
c = Client("https://connect.example", "12345")
58+
c.ctx.version = "2024.08.0"
5859
# invoke
5960
associations = c.oauth.integrations.get(guid).associations.find()
6061

@@ -83,6 +84,7 @@ def test(self):
8384

8485
# setup
8586
c = Client("https://connect.example", "12345")
87+
c.ctx.version = "2024.08.0"
8688
# invoke
8789
associations = c.content.get(guid).oauth.associations.find()
8890

@@ -115,6 +117,7 @@ def test(self):
115117

116118
# setup
117119
c = Client("https://connect.example", "12345")
120+
c.ctx.version = "2024.08.0"
118121

119122
# invoke
120123
c.content.get(guid).oauth.associations.update(new_integration_guid)
@@ -142,6 +145,7 @@ def test(self):
142145

143146
# setup
144147
c = Client("https://connect.example", "12345")
148+
c.ctx.version = "2024.08.0"
145149

146150
# invoke
147151
c.content.get(guid).oauth.associations.delete()

tests/posit/connect/oauth/test_integrations.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def test(self):
7373

7474
# setup
7575
c = Client("https://connect.example", "12345")
76+
c.ctx.version = "2024.08.0"
7677
integration = c.oauth.integrations.get(guid)
7778

7879
# invoke
@@ -93,6 +94,7 @@ def test(self):
9394
)
9495

9596
c = Client("https://connect.example", "12345")
97+
c.ctx.version = "2024.08.0"
9698
integration = c.oauth.integrations.get(guid)
9799
assert integration.guid == guid
98100

@@ -137,6 +139,7 @@ def test(self):
137139

138140
# setup
139141
c = Client("https://connect.example", "12345")
142+
c.ctx.version = "2024.08.0"
140143

141144
# invoke
142145
integration = c.oauth.integrations.create(
@@ -164,10 +167,11 @@ def test(self):
164167
)
165168

166169
# setup
167-
client = Client("https://connect.example", "12345")
170+
c = Client("https://connect.example", "12345")
171+
c.ctx.version = "2024.08.0"
168172

169173
# invoke
170-
integrations = client.oauth.integrations.find()
174+
integrations = c.oauth.integrations.find()
171175

172176
# assert
173177
assert mock_get.call_count == 1
@@ -189,6 +193,7 @@ def test(self):
189193

190194
# setup
191195
c = Client("https://connect.example", "12345")
196+
c.ctx.version = "2024.08.0"
192197
integration = c.oauth.integrations.get(guid)
193198

194199
assert mock_get.call_count == 1

tests/posit/connect/oauth/test_oauth.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ def test_get_credentials(self):
2323
"token_type": "Bearer",
2424
},
2525
)
26-
con = Client(api_key="12345", url="https://connect.example/")
27-
assert con.oauth.get_credentials("cit")["access_token"] == "viewer-token"
26+
c = Client(api_key="12345", url="https://connect.example/")
27+
c.ctx.version = "2024.08.0"
28+
assert c.oauth.get_credentials("cit")["access_token"] == "viewer-token"

tests/posit/connect/oauth/test_sessions.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def test(self):
5353

5454
# setup
5555
c = Client("https://connect.example", "12345")
56+
c.ctx.version = "2024.08.0"
5657
session = c.oauth.sessions.get(guid)
5758

5859
# invoke
@@ -72,10 +73,11 @@ def test(self):
7273
)
7374

7475
# setup
75-
client = Client("https://connect.example", "12345")
76+
c = Client("https://connect.example", "12345")
77+
c.ctx.version = "2024.08.0"
7678

7779
# invoke
78-
sessions = client.oauth.sessions.find()
80+
sessions = c.oauth.sessions.find()
7981

8082
# assert
8183
assert mock_get.call_count == 1
@@ -94,10 +96,11 @@ def test_params_all(self):
9496
)
9597

9698
# setup
97-
client = Client("https://connect.example", "12345")
99+
c = Client("https://connect.example", "12345")
100+
c.ctx.version = "2024.08.0"
98101

99102
# invoke
100-
client.oauth.sessions.find(all=True)
103+
c.oauth.sessions.find(all=True)
101104

102105
# assert
103106
assert mock_get.call_count == 1
@@ -115,10 +118,11 @@ def test(self):
115118
)
116119

117120
# setup
118-
client = Client("https://connect.example", "12345")
121+
c = Client("https://connect.example", "12345")
122+
c.ctx.version = "2024.08.0"
119123

120124
# invoke
121-
session = client.oauth.sessions.get(guid=guid)
125+
session = c.oauth.sessions.get(guid=guid)
122126

123127
# assert
124128
assert mock_get.call_count == 1
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from email.contentmanager import ContentManager
2+
from unittest.mock import MagicMock, Mock
3+
4+
import pytest
5+
import requests
6+
import responses
7+
8+
from posit.connect.context import Context, requires
9+
from posit.connect.urls import Url
10+
11+
12+
class TestRequires:
13+
def test_version_unsupported(self):
14+
class Stub(ContentManager):
15+
def __init__(self, ctx):
16+
self.ctx = ctx
17+
18+
@requires("1.0.0")
19+
def fail(self):
20+
pass
21+
22+
ctx = MagicMock()
23+
ctx.version = "0.0.0"
24+
instance = Stub(ctx)
25+
26+
with pytest.raises(RuntimeError):
27+
instance.fail()
28+
29+
def test_version_supported(self):
30+
class Stub(ContentManager):
31+
def __init__(self, ctx):
32+
self.ctx = ctx
33+
34+
@requires("1.0.0")
35+
def success(self):
36+
pass
37+
38+
ctx = MagicMock()
39+
ctx.version = "1.0.0"
40+
instance = Stub(ctx)
41+
42+
instance.success()
43+
44+
def test_version_missing(self):
45+
class Stub(ContentManager):
46+
def __init__(self, ctx):
47+
self.ctx = ctx
48+
49+
@requires("1.0.0")
50+
def success(self):
51+
pass
52+
53+
ctx = MagicMock()
54+
ctx.version = None
55+
instance = Stub(ctx)
56+
57+
instance.success()
58+
59+
60+
class TestContextVersion:
61+
@responses.activate
62+
def test_unknown(self):
63+
responses.get(
64+
f"http://connect.example/__api__/server_settings",
65+
json={},
66+
)
67+
68+
session = requests.Session()
69+
url = Url("http://connect.example")
70+
ctx = Context(session, url)
71+
72+
assert ctx.version is None
73+
74+
@responses.activate
75+
def test_known(self):
76+
responses.get(
77+
f"http://connect.example/__api__/server_settings",
78+
json={"version": "2024.09.24"},
79+
)
80+
81+
session = requests.Session()
82+
url = Url("http://connect.example")
83+
ctx = Context(session, url)
84+
85+
assert ctx.version == "2024.09.24"
86+
87+
def test_setter(self):
88+
ctx = Context(Mock(), Mock())
89+
ctx.version = "2024.09.24"
90+
assert ctx.version == "2024.09.24"

0 commit comments

Comments
 (0)