Skip to content

Commit 0e9cf02

Browse files
authored
feat: add version check support (#299)
Adds a new decorator, `@context.requires`, which asserts version compatibility when the server version is known. The check is skipped if the server version is unknown (e.g., the Connect configuration disables version information). Also marks the OAuth API with a '2024.08.0' requirement. Closes #272
1 parent 0614ae4 commit 0e9cf02

File tree

12 files changed

+185
-15
lines changed

12 files changed

+185
-15
lines changed

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ classifiers = [
1919
"Typing :: Typed",
2020
]
2121
dynamic = ["version"]
22-
dependencies = ["requests>=2.31.0,<3"]
22+
dependencies = [
23+
"requests>=2.31.0,<3",
24+
"packaging"
25+
]
2326

2427
[project.urls]
2528
Source = "https://github.com/posit-dev/posit-sdk-py"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
requests==2.32.2
2+
packaging==24.1

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

0 commit comments

Comments
 (0)