Skip to content

Commit e57498f

Browse files
committed
--wip-- [skip ci]
1 parent 8efbecf commit e57498f

File tree

14 files changed

+171
-70
lines changed

14 files changed

+171
-70
lines changed

src/posit/connect/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .groups import Groups
1515
from .metrics import Metrics
1616
from .oauth import OAuth
17-
from .packages import Packages
17+
from .packages import GlobalPackages
1818
from .resources import ResourceParameters
1919
from .tasks import Tasks
2020
from .users import User, Users
@@ -156,7 +156,7 @@ def __init__(self, *args, **kwargs) -> None:
156156
session.hooks["response"].append(hooks.handle_errors)
157157
self.session = session
158158
self.resource_params = ResourceParameters(session, self.cfg.url)
159-
self.ctx = Context(self.session, self.cfg.url)
159+
self._ctx = Context(self.session, self.cfg.url)
160160

161161
@property
162162
def version(self) -> Optional[str]:
@@ -168,7 +168,7 @@ def version(self) -> Optional[str]:
168168
str
169169
The version of the Posit Connect server.
170170
"""
171-
return self.ctx.version
171+
return self._ctx.version
172172

173173
@property
174174
def me(self) -> User:
@@ -271,8 +271,8 @@ def oauth(self) -> OAuth:
271271
return OAuth(self.resource_params, self.cfg.api_key)
272272

273273
@property
274-
def packages(self) -> Packages:
275-
return Packages(self.ctx, "v1/packages")
274+
def packages(self) -> GlobalPackages:
275+
return GlobalPackages(self._ctx, "v1/packages")
276276

277277
@property
278278
def vanities(self) -> Vanities:

src/posit/connect/content.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .env import EnvVars
1414
from .jobs import JobsMixin
1515
from .oauth.associations import ContentItemAssociations
16-
from .packages import PackagesMixin
16+
from .packages.packages import PackagesMixin
1717
from .permissions import Permissions
1818
from .resources import Resource, ResourceParameters, Resources
1919
from .vanities import VanityMixin

src/posit/connect/context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def requires(version: str):
1111
def decorator(func):
1212
@functools.wraps(func)
1313
def wrapper(instance: ContextManager, *args, **kwargs):
14-
ctx = instance.ctx
14+
ctx = instance._ctx
1515
if ctx.version and Version(ctx.version) < Version(version):
1616
raise RuntimeError(
1717
f"This API is not available in Connect version {ctx.version}. Please upgrade to version {version} or later.",
@@ -45,4 +45,4 @@ def version(self, value):
4545

4646

4747
class ContextManager(Protocol):
48-
ctx: Context
48+
_ctx: Context

src/posit/connect/packages.py

Lines changed: 129 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,171 @@
11
import posixpath
2-
from typing import Optional, TypedDict, overload
2+
from typing import Literal, Optional, TypedDict, overload
33

44
from typing_extensions import NotRequired, Required, Unpack
55

6+
from posit.connect.context import requires
7+
from posit.connect.paginator import Paginator
8+
69
from .resources import Active, ActiveFinderMethods, ActiveSequence
710

811

9-
class Package(Active):
12+
class ContentPackage(Active):
1013
class _Package(TypedDict):
1114
language: Required[str]
1215
name: Required[str]
1316
version: Required[str]
1417
hash: Required[Optional[str]]
1518

16-
def __init__(self, ctx, path, /, **attributes: Unpack[_Package]):
17-
super().__init__(ctx, path, **attributes)
19+
def __init__(self, ctx, /, **attributes: Unpack[_Package]):
20+
# todo - passing "" is a hack since path isn't needed. Instead, this class should inherit from Resource, but ActiveSequence is designed to operate on Active. That should change.
21+
super().__init__(ctx, "", **attributes)
1822

1923

20-
class Packages(ActiveFinderMethods["Package"], ActiveSequence["Package"]):
24+
class ContentPackages(ActiveFinderMethods["ContentPackage"], ActiveSequence["ContentPackage"]):
2125
"""A collection of packages."""
2226

2327
def __init__(self, ctx, path):
2428
super().__init__(ctx, path, "name")
2529

2630
def _create_instance(self, path, /, **attributes):
27-
return Package(self._ctx, path, **attributes)
31+
return ContentPackage(self._ctx, **attributes)
32+
33+
def find(self, uid):
34+
raise NotImplementedError("The 'find' method is not support by the Packages API.")
2835

2936
class _FindBy(TypedDict, total=False):
30-
language: NotRequired[str]
37+
language: NotRequired[Literal["python", "r"]]
38+
"""Programming language ecosystem, options are 'python' and 'r'"""
39+
3140
name: NotRequired[str]
41+
"""The package name"""
42+
3243
version: NotRequired[str]
44+
"""The package version"""
45+
3346
hash: NotRequired[Optional[str]]
47+
"""Package description hash for R packages."""
3448

3549
@overload
3650
def find_by(self, **conditions: Unpack[_FindBy]):
37-
...
51+
"""
52+
Find the first record matching the specified conditions.
53+
54+
There is no implied ordering, so if order matters, you should specify it yourself.
55+
56+
Parameters
57+
----------
58+
**conditions : Unpack[_FindBy]
59+
Conditions for filtering packages. The following keys are accepted:
60+
61+
language : {"python", "r"}, not required
62+
Programming language ecosystem, options are 'python' and 'r'
63+
64+
name : str, not required
65+
The package name
66+
67+
version : str, not required
68+
The package version
69+
70+
hash : str or None, optional, not required
71+
Package description hash for R packages.
72+
73+
Returns
74+
-------
75+
Optional[T]
76+
The first record matching the specified conditions, or `None` if no such record exists.
77+
"""
3878

3979
@overload
40-
def find_by(self, **conditions):
41-
...
80+
def find_by(self, **conditions): ...
4281

4382
def find_by(self, **conditions):
4483
return super().find_by(**conditions)
4584

46-
class PackagesMixin(Active):
85+
86+
class ContentPackagesMixin(Active):
4787
"""Mixin class to add a packages attribute."""
4888

49-
def __init__(self, ctx, path, /, **attributes):
50-
"""Mixin class which adds a `packages` attribute.
89+
@property
90+
@requires(version="2024.11.0")
91+
def packages(self):
92+
path = posixpath.join(self._path, "packages")
93+
return ContentPackages(self._ctx, path)
94+
95+
96+
class GlobalPackage(Active):
97+
class _GlobalPackage(TypedDict):
98+
language: Required[str]
99+
name: Required[str]
100+
version: Required[str]
101+
hash: Required[Optional[str]]
102+
103+
def __init__(self, ctx, /, **attributes: Unpack[_GlobalPackage]):
104+
# todo - passing "" is a hack since path isn't needed. Instead, this class should inherit from Resource, but ActiveSequence is designed to operate on Active. That should change.
105+
super().__init__(ctx, "", **attributes)
106+
107+
108+
class GlobalPackages(ContentPackages):
109+
def __init__(self, ctx, path):
110+
super().__init__(ctx, path, "name")
111+
112+
def _create_instance(self, path, /, **attributes):
113+
return ContentPackage(self._ctx, **attributes)
114+
115+
def find(self, uid):
116+
raise NotImplementedError("The 'find' method is not support by the Packages API.")
117+
118+
class _FindBy(TypedDict, total=False):
119+
language: NotRequired[Literal["python", "r"]]
120+
"""Programming language ecosystem, options are 'python' and 'r'"""
121+
122+
name: NotRequired[str]
123+
"""The package name"""
124+
125+
version: NotRequired[str]
126+
"""The package version"""
127+
128+
hash: NotRequired[Optional[str]]
129+
"""Package description hash for R packages."""
130+
131+
def fetch(self, **conditions):
132+
url = self._ctx.url + self._path
133+
paginator = Paginator(self._ctx.session, url, conditions)
134+
results = paginator.fetch_results()
135+
return [self._create_instance("", **result) for result in results]
136+
137+
@overload
138+
def find_by(self, **conditions: Unpack[_FindBy]):
139+
"""
140+
Find the first record matching the specified conditions.
141+
142+
There is no implied ordering, so if order matters, you should specify it yourself.
51143
52144
Parameters
53145
----------
54-
ctx : Context
55-
The context object containing the session and URL for API interactions
56-
path : str
57-
The HTTP path component for the resource endpoint
58-
**attributes : dict
59-
Resource attributes passed
146+
**conditions : Unpack[_FindBy]
147+
Conditions for filtering packages. The following keys are accepted:
148+
149+
language : {"python", "r"}, not required
150+
Programming language ecosystem, options are 'python' and 'r'
151+
152+
name : str, not required
153+
The package name
154+
155+
version : str, not required
156+
The package version
157+
158+
hash : str or None, optional, not required
159+
Package description hash for R packages.
160+
161+
Returns
162+
-------
163+
Optional[T]
164+
The first record matching the specified conditions, or `None` if no such record exists.
60165
"""
61-
super().__init__(ctx, path, **attributes)
62166

63-
path = posixpath.join(path, "packages")
64-
self.packages = Packages(ctx, path)
167+
@overload
168+
def find_by(self, **conditions): ...
169+
170+
def find_by(self, **conditions):
171+
return super().find_by(**conditions)

src/posit/connect/resources.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from __future__ import annotations
2+
13
import posixpath
24
import warnings
35
from abc import ABC, abstractmethod
4-
from copy import copy
56
from dataclasses import dataclass
67
from typing import Any, Generic, List, Optional, Sequence, TypeVar, overload
78

@@ -82,7 +83,7 @@ class ActiveSequence(ABC, Generic[T], Sequence[T]):
8283

8384
_cache: Optional[List[T]]
8485

85-
def __init__(self, ctx: Context, path: str, uid: str = "guid", params: dict = {}):
86+
def __init__(self, ctx: Context, path: str, uid: str = "guid"):
8687
"""A sequence abstraction for any HTTP GET endpoint that returns a collection.
8788
8889
Parameters
@@ -98,15 +99,14 @@ def __init__(self, ctx: Context, path: str, uid: str = "guid", params: dict = {}
9899
self._ctx = ctx
99100
self._path = path
100101
self._uid = uid
101-
self._params = params
102102
self._cache = None
103103

104104
@abstractmethod
105105
def _create_instance(self, path: str, /, **kwargs: Any) -> T:
106106
"""Create an instance of 'T'."""
107107
raise NotImplementedError()
108108

109-
def fetch(self) -> List[T]:
109+
def fetch(self, **conditions) -> List[T]:
110110
"""Fetch the collection.
111111
112112
Fetches the collection directly from Connect. This operation does not effect the cache state.
@@ -116,7 +116,7 @@ def fetch(self) -> List[T]:
116116
List[T]
117117
"""
118118
endpoint = self._ctx.url + self._path
119-
response = self._ctx.session.get(endpoint, params=self._params)
119+
response = self._ctx.session.get(endpoint, params=conditions)
120120
results = response.json()
121121
return [self._to_instance(result) for result in results]
122122

@@ -203,7 +203,7 @@ def find(self, uid) -> T:
203203
result = response.json()
204204
return self._to_instance(result)
205205

206-
def find_by(self, **conditions: Any) -> Optional[T]:
206+
def find_by(self, **conditions) -> Optional[T]:
207207
"""
208208
Find the first record matching the specified conditions.
209209

tests/posit/connect/external/test_databricks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +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
51+
client._ctx.version = None
5252
cp = PositCredentialsProvider(client=client, user_session_token="cit")
5353
assert cp() == {"Authorization": "Bearer dynamic-viewer-access-token"}
5454

@@ -58,7 +58,7 @@ def test_posit_credentials_strategy(self):
5858
register_mocks()
5959

6060
client = Client(api_key="12345", url="https://connect.example/")
61-
client.ctx.version = None
61+
client._ctx.version = None
6262
cs = PositCredentialsStrategy(
6363
local_strategy=mock_strategy(),
6464
user_session_token="cit",

tests/posit/connect/external/test_snowflake.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +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
36+
client._ctx.version = None
3737
auth = PositAuthenticator(
3838
local_authenticator="SNOWFLAKE",
3939
user_session_token="cit",
@@ -45,7 +45,7 @@ def test_posit_authenticator(self):
4545
def test_posit_authenticator_fallback(self):
4646
# local_authenticator is used when the content is running locally
4747
client = Client(api_key="12345", url="https://connect.example/")
48-
client.ctx.version = None
48+
client._ctx.version = None
4949
auth = PositAuthenticator(
5050
local_authenticator="SNOWFLAKE",
5151
user_session_token="cit",

tests/posit/connect/oauth/test_associations.py

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

5656
# setup
5757
c = Client("https://connect.example", "12345")
58-
c.ctx.version = None
58+
c._ctx.version = None
5959
# invoke
6060
associations = c.oauth.integrations.get(guid).associations.find()
6161

@@ -84,7 +84,7 @@ def test(self):
8484

8585
# setup
8686
c = Client("https://connect.example", "12345")
87-
c.ctx.version = None
87+
c._ctx.version = None
8888
# invoke
8989
associations = c.content.get(guid).oauth.associations.find()
9090

@@ -117,7 +117,7 @@ def test(self):
117117

118118
# setup
119119
c = Client("https://connect.example", "12345")
120-
c.ctx.version = None
120+
c._ctx.version = None
121121

122122
# invoke
123123
c.content.get(guid).oauth.associations.update(new_integration_guid)
@@ -145,7 +145,7 @@ def test(self):
145145

146146
# setup
147147
c = Client("https://connect.example", "12345")
148-
c.ctx.version = None
148+
c._ctx.version = None
149149

150150
# invoke
151151
c.content.get(guid).oauth.associations.delete()

0 commit comments

Comments
 (0)