Skip to content

Commit 68a8901

Browse files
committed
refactor: use Unpack and TypedDict for pass-through arguments
1 parent 13d22ab commit 68a8901

File tree

4 files changed

+65
-202
lines changed

4 files changed

+65
-202
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ classifiers = [
2121
dynamic = ["version"]
2222
dependencies = [
2323
"requests>=2.31.0,<3",
24-
"packaging"
24+
"packaging",
25+
"typing-extensions"
2526
]
2627

2728
[project.urls]

requirements.txt

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

src/posit/connect/paginator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class Paginator:
3838
url (str): The URL of the paginated API endpoint.
3939
"""
4040

41-
def __init__(self, session: requests.Session, url: str, params: dict = {}) -> None:
41+
def __init__(self, session: requests.Session, url: str, params = {}) -> None:
4242
self.session = session
4343
self.url = url
4444
self.params = params

src/posit/connect/users.py

Lines changed: 61 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from __future__ import annotations
44

5-
from typing import List, Literal, Optional, overload
5+
from typing import List, Literal, TypedDict
6+
7+
from typing_extensions import NotRequired, Required, Unpack
68

79
from . import me
810
from .content import Content
@@ -71,31 +73,33 @@ def unlock(self):
7173
self.params.session.post(url, json=body)
7274
super().update(locked=False)
7375

74-
@overload
76+
class UpdateUser(TypedDict):
77+
"""Update user request."""
78+
79+
email: NotRequired[str]
80+
username: NotRequired[str]
81+
first_name: NotRequired[str]
82+
last_name: NotRequired[str]
83+
user_role: NotRequired[Literal["administrator", "publisher", "viewer"]]
84+
7585
def update(
7686
self,
77-
*args,
78-
email: Optional[str] = None,
79-
username: Optional[str] = None,
80-
first_name: Optional[str] = None,
81-
last_name: Optional[str] = None,
82-
user_role: Optional[Literal["administrator", "publisher", "viewer"]] = None,
83-
**kwargs,
87+
**kwargs: Unpack[UpdateUser],
8488
) -> None:
8589
"""
8690
Update the user's attributes.
8791
8892
Parameters
8993
----------
90-
email : str, optional
94+
email : str, not required
9195
The new email address for the user. Default is `None`.
92-
username : str, optional
96+
username : str, not required
9397
The new username for the user. Default is `None`.
94-
first_name : str, optional
98+
first_name : str, not required
9599
The new first name for the user. Default is `None`.
96-
last_name : str, optional
100+
last_name : str, not required
97101
The new last name for the user. Default is `None`.
98-
user_role : Literal["administrator", "publisher", "viewer"], optional
102+
user_role : Literal["administrator", "publisher", "viewer"], not required
99103
The new role for the user. Options are `'administrator'`, `'publisher'`, `'viewer'`. Default is `None`.
100104
101105
Returns
@@ -112,44 +116,8 @@ def update(
112116
113117
>>> user.update(first_name="Jane", last_name="Smith")
114118
"""
115-
...
116-
117-
@overload
118-
def update(self, *args, **kwargs) -> None:
119-
"""
120-
Update the user.
121-
122-
Parameters
123-
----------
124-
*args
125-
Variable length argument list.
126-
**kwargs
127-
Arbitrary keyword arguments.
128-
129-
Returns
130-
-------
131-
None
132-
"""
133-
...
134-
135-
def update(self, *args, **kwargs) -> None:
136-
"""
137-
Update the user.
138-
139-
Parameters
140-
----------
141-
*args
142-
Variable length argument list.
143-
**kwargs
144-
Arbitrary keyword arguments.
145-
146-
Returns
147-
-------
148-
None
149-
"""
150-
body = dict(*args, **kwargs)
151119
url = self.params.url + f"v1/users/{self['guid']}"
152-
response = self.params.session.put(url, json=body)
120+
response = self.params.session.put(url, json=kwargs)
153121
super().update(**response.json())
154122

155123

@@ -159,46 +127,45 @@ class Users(Resources):
159127
def __init__(self, params: ResourceParameters) -> None:
160128
super().__init__(params)
161129

162-
@overload
163-
def create(
164-
self,
165-
*,
166-
# Required arguments
167-
username: str,
130+
class CreateUser(TypedDict):
131+
"""Create user request."""
132+
133+
username: Required[str]
168134
# Authentication Information
169-
password: Optional[str] = None,
170-
user_must_set_password: bool = False,
135+
password: NotRequired[str]
136+
user_must_set_password: NotRequired[bool]
171137
# Profile Information
172-
email: Optional[str] = None,
173-
first_name: Optional[str] = None,
174-
last_name: Optional[str] = None,
138+
email: NotRequired[str]
139+
first_name: NotRequired[str]
140+
last_name: NotRequired[str]
175141
# Role and Permissions
176-
user_role: Optional[Literal["administrator", "publisher", "viewer"]] = None,
177-
unique_id: Optional[str] = None,
178-
) -> User:
142+
user_role: NotRequired[Literal["administrator", "publisher", "viewer"]]
143+
unique_id: NotRequired[str]
144+
145+
def create(self, **attributes: Unpack[CreateUser]) -> User:
179146
"""
180147
Create a new user with the specified attributes.
181148
182149
Applies when server setting 'Authentication.Provider' is set to 'ldap', 'oauth2', 'pam', 'password', 'proxy', or 'saml'.
183150
184151
Parameters
185152
----------
186-
username : str
153+
username : str, required
187154
The user's desired username.
188-
password : str, optional
189-
Applies when server setting 'Authentication.Provider="password"'. Cannot be set when `user_must_set_password` is `True`. Default is `None`.
190-
user_must_set_password : bool, optional
155+
password : str, not required
156+
Applies when server setting 'Authentication.Provider="password"'. Cannot be set when `user_must_set_password` is `True`.
157+
user_must_set_password : bool, not required
191158
If `True`, the user is prompted to set their password on first login. When `False`, the `password` parameter is used. Default is `False`. Applies when server setting 'Authentication.Provider="password"'.
192-
email : str, optional
193-
The user's email address. Default is `None`.
194-
first_name : str, optional
195-
The user's first name. Default is `None`.
196-
last_name : str, optional
197-
The user's last name. Default is `None`.
198-
user_role : Literal["administrator", "publisher", "viewer"], optional
199-
The user role. Default is `None`. Options are `'administrator'`, `'publisher'`, `'viewer'`. Falls back to server setting 'Authorization.DefaultUserRole'.
200-
unique_id : str, optional
201-
Default is `None`. Required when server is configured with SAML or OAuth2 (non-Google) authentication. Applies when server setting `ProxyAuth.UniqueIdHeader` is set.
159+
email : str, not required
160+
The user's email address.
161+
first_name : str, not required
162+
The user's first name.
163+
last_name : str, not required
164+
The user's last name.
165+
user_role : Literal["administrator", "publisher", "viewer"], not required
166+
The user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Falls back to server setting 'Authorization.DefaultUserRole'.
167+
unique_id : str, maybe required
168+
Required when server is configured with SAML or OAuth2 (non-Google) authentication. Applies when server setting `ProxyAuth.UniqueIdHeader` is set.
202169
203170
Returns
204171
-------
@@ -229,63 +196,30 @@ def create(
229196
... user_role="viewer",
230197
... )
231198
"""
232-
...
233-
234-
@overload
235-
def create(self, **attributes) -> User:
236-
"""
237-
Create a user with the specified attributes.
238-
239-
Parameters
240-
----------
241-
**attributes
242-
Arbitrary keyword arguments representing user attributes.
243-
244-
Returns
245-
-------
246-
User
247-
The newly created user.
248-
"""
249-
...
250-
251-
def create(self, **attributes) -> User:
252-
"""
253-
Create a user.
254-
255-
Parameters
256-
----------
257-
**attributes
258-
Arbitrary keyword arguments representing user attributes.
259-
260-
Returns
261-
-------
262-
User
263-
The newly created user.
264-
"""
265199
# todo - use the 'context' module to inspect the 'authentication' object and route to POST (local) or PUT (remote).
266200
url = self.params.url + "v1/users"
267201
response = self.params.session.post(url, json=attributes)
268202
return User(self.params, **response.json())
269203

270-
@overload
271-
def find(
272-
self,
273-
*,
274-
prefix: Optional[str] = None,
275-
user_role: Optional[Literal["administrator", "publisher", "viewer"] | str] = None,
276-
account_status: Optional[Literal["locked", "licensed", "inactive"] | str] = None,
277-
) -> List[User]:
204+
class FindUser(TypedDict):
205+
"""Find user request."""
206+
207+
prefix: NotRequired[str]
208+
user_role: NotRequired[Literal["administrator", "publisher", "viewer"] | str]
209+
account_status: NotRequired[Literal["locked", "licensed", "inactive"] | str]
210+
211+
def find(self, **conditions: Unpack[FindUser]) -> List[User]:
278212
"""
279213
Find users matching the specified conditions.
280214
281215
Parameters
282216
----------
283-
prefix : str, optional
284-
Filter users by prefix (username, first name, or last name). The filter is case-insensitive. Default is `None`.
285-
user_role : Literal["administrator", "publisher", "viewer"], optional
286-
Filter by user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Use `'|'` to represent logical OR (e.g., `'viewer|publisher'`). Default is `None`.
287-
account_status : Literal["locked", "licensed", "inactive"], optional
288-
Filter by account status. Options are `'locked'`, `'licensed'`, `'inactive'`. Use `'|'` to represent logical OR. For example, `'locked|licensed'` includes users who are either locked or licensed. Default is `None`.
217+
prefix : str, not required
218+
Filter users by prefix (username, first name, or last name). The filter is case-insensitive.
219+
user_role : Literal["administrator", "publisher", "viewer"], not required
220+
Filter by user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Use `'|'` to represent logical OR (e.g., `'viewer|publisher'`).
221+
account_status : Literal["locked", "licensed", "inactive"], not required
222+
Filter by account status. Options are `'locked'`, `'licensed'`, `'inactive'`. Use `'|'` to represent logical OR. For example, `'locked|licensed'` includes users who are either locked or licensed.
289223
290224
Returns
291225
-------
@@ -306,39 +240,6 @@ def find(
306240
307241
>>> users = client.find(account_status="locked|licensed")
308242
"""
309-
...
310-
311-
@overload
312-
def find(self, **conditions) -> List[User]:
313-
"""
314-
Find users matching the specified conditions.
315-
316-
Parameters
317-
----------
318-
**conditions
319-
Arbitrary keyword arguments representing search conditions.
320-
321-
Returns
322-
-------
323-
List[User]
324-
A list of users matching the specified conditions.
325-
"""
326-
...
327-
328-
def find(self, **conditions) -> List[User]:
329-
"""
330-
Find users matching the specified conditions.
331-
332-
Parameters
333-
----------
334-
**conditions
335-
Arbitrary keyword arguments representing search conditions.
336-
337-
Returns
338-
-------
339-
List[User]
340-
A list of users matching the specified conditions.
341-
"""
342243
url = self.params.url + "v1/users"
343244
paginator = Paginator(self.params.session, url, params=conditions)
344245
results = paginator.fetch_results()
@@ -350,14 +251,7 @@ def find(self, **conditions) -> List[User]:
350251
for user in results
351252
]
352253

353-
@overload
354-
def find_one(
355-
self,
356-
*,
357-
prefix: Optional[str] = None,
358-
user_role: Optional[Literal["administrator", "publisher", "viewer"] | str] = None,
359-
account_status: Optional[Literal["locked", "licensed", "inactive"] | str] = None,
360-
) -> User | None:
254+
def find_one(self, **conditions: Unpack[FindUser]) -> User | None:
361255
"""
362256
Find a user matching the specified conditions.
363257
@@ -389,39 +283,6 @@ def find_one(
389283
390284
>>> user = client.find_one(account_status="locked|licensed")
391285
"""
392-
...
393-
394-
@overload
395-
def find_one(self, **conditions) -> User | None:
396-
"""
397-
Find a user matching the specified conditions.
398-
399-
Parameters
400-
----------
401-
**conditions
402-
Arbitrary keyword arguments representing search conditions.
403-
404-
Returns
405-
-------
406-
User or None
407-
The first user matching the specified conditions, or `None` if no user is found.
408-
"""
409-
...
410-
411-
def find_one(self, **conditions) -> User | None:
412-
"""
413-
Find a user matching the specified conditions.
414-
415-
Parameters
416-
----------
417-
**conditions
418-
Arbitrary keyword arguments representing search conditions.
419-
420-
Returns
421-
-------
422-
User or None
423-
The first user matching the specified conditions, or `None` if no user is found.
424-
"""
425286
url = self.params.url + "v1/users"
426287
paginator = Paginator(self.params.session, url, params=conditions)
427288
pages = paginator.fetch_pages()

0 commit comments

Comments
 (0)