You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
> Note: this is design-by-wishful-thinking, not how things actually work today.
4
4
> To discuss or propose changes, open a PR suggesting new language.
5
+
5
6
### Connecting
6
7
7
8
To get started, import the Connect `Client` and create a connection. You can specify the `endpoint` for your Connect server URL and your `api_key`; if not specified, they'll be pulled from the environment (`CONNECT_SERVER` and `CONNECT_API_KEY`).
8
9
9
10
It is expected that `Client()` just works from within any Posit product's environment (Workbench, Connect, etc.), either by API key and prior system configuration, or by some means of identity federation.
10
11
11
-
```
12
+
```python
12
13
from posit.connect import Client
13
14
14
15
con = Client()
@@ -18,31 +19,31 @@ con = Client()
18
19
19
20
Many resources in the SDK refer to *collections* of *entities* or records in Connect.
20
21
21
-
All of the general collections can be referenced as properties of the Client object (e.g. `client.content`, `client.users`). Some collections belong to a single entity and are referenced from them similarly (e.g. `content_item.permissions`).
22
+
All of the general collections can be referenced as properties of the Client object (e.g. `client.content`, `client.users`). Some collections belong to a single entity and are referenced from them similarly (e.g. `content_item.permissions`).
22
23
23
24
All collections are iterable objects with all read-only List-like methods implemented. They also have the following methods:
24
25
25
-
*`.find()`: returns another iterable collection object.
26
-
* Calling `.find()` with no arguments retrieves all available entities
27
-
* If no entities match the query, `.find()` returns a length-0 collection.
28
-
* Iterating over a collection without having first called `find()` is equivalent to having queried for all.
29
-
*`find()` should use query-based REST APIs where existing, and fall back to retrieving all and filtering client-side where those APIs do not (yet) exist.
30
-
* Should `collection.find().find()` work? Probably.
26
+
*`.find()`: returns another iterable collection object.
27
+
* Calling `.find()` with no arguments retrieves all available entities
28
+
* If no entities match the query, `.find()` returns a length-0 collection.
29
+
* Iterating over a collection without having first called `find()` is equivalent to having queried for all.
30
+
*`find()` should use query-based REST APIs where existing, and fall back to retrieving all and filtering client-side where those APIs do not (yet) exist.
31
+
* Should `collection.find().find()` work? Probably.
31
32
*`.get(guid)` method that returns a single entity by id. If one is not found, it raises `NotFoundError`
32
33
*`.find_one()` is a convenience method that queries with `.find()` and returns a single entity
33
-
* If more than one entity match the query, `.find_one()` returns the first
34
-
* If no entities match, `.find_one()` returns `None`
35
-
* If you need stricter behavior (e.g. you want to be sure that one and only one entity are returned by your query), use `.find()` or `.get()`.
36
-
*`.to_pandas()` materializes the collection in a pandas `DataFrame`.
37
-
* pandas is not a required dependency of the SDK. `.to_pandas()` should try to import inside the method.
34
+
* If more than one entity match the query, `.find_one()` returns the first
35
+
* If no entities match, `.find_one()` returns `None`
36
+
* If you need stricter behavior (e.g. you want to be sure that one and only one entity are returned by your query), use `.find()` or `.get()`.
37
+
*`.to_pandas()` materializes the collection in a pandas `DataFrame`.
38
+
* pandas is not a required dependency of the SDK. `.to_pandas()` should try to import inside the method.
38
39
39
-
The `.find()` and `.find_one()` methods use named arguments rather than accepting a dict so that IDE tab completion can work.
40
+
The `.find()` and `.find_one()` methods use named arguments rather than accepting a dict so that IDE tab completion can work.
40
41
41
-
Collections should handle all API reponse pagination invisibly so that the Python user doesn't need to worry about pages.
42
+
Collections should handle all API reponse pagination invisibly so that the Python user doesn't need to worry about pages.
42
43
43
-
Entities have methods that are appropriate to them. Fields in the entity bodies can be accessed as properties.
44
+
Entities have methods that are appropriate to them. Fields in the entity bodies can be accessed as properties.
44
45
45
-
```
46
+
```python
46
47
for st in con.content.find(app_mode="streamlit"):
47
48
print(st.title)
48
49
@@ -53,30 +54,168 @@ for perm in my_app.permissions:
53
54
54
55
### Mapping to HTTP request methods
55
56
56
-
Entities have an `.update()` method that maps to a `PATCH` request. `.delete()` is `DELETE`.
57
+
Entities have an `.update()` method that maps to a `PATCH` request. `.delete()` is `DELETE`.
57
58
58
-
```
59
+
```python
59
60
my_app.update(title="Quarterly Analysis of Team Velocity")
The Python SDK should present the interface we wish we had, and we can evolve the REST API to match that over time. It is the adapter layer that allows us to evolve the Connect API more freely.
73
+
The Python SDK should present the interface we wish we had, and we can evolve the REST API to match that over time. It is the adapter layer that allows us to evolve the Connect API more freely.
73
74
74
-
Naming of fields and arguments in collection and entity methods should be standardized across entity types for consistency, even if this creates a gap between our current REST API specification.
75
+
Naming of fields and arguments in collection and entity methods should be standardized across entity types for consistency, even if this creates a gap between our current REST API specification.
75
76
76
-
As a result, the SDK takes on the burden of smoothing over the changes in the Connect API over time. Each collection and entity class may need its own adapter methods that take the current Python SDK field names and maps to the values for the version of the Connect server being used when passing to the HTTP methods.
77
+
As a result, the SDK takes on the burden of smoothing over the changes in the Connect API over time. Each collection and entity class may need its own adapter methods that take the current Python SDK field names and maps to the values for the version of the Connect server being used when passing to the HTTP methods.
77
78
78
79
Entity `.to_dict()` methods likewise present the names and values in the Python interface, which may not map to the actual HTTP response body JSON. There should be some other way to access the raw response body.
79
80
80
81
### Lower-level HTTP interface
81
82
82
-
The client object has `.get`, `.post`, etc. methods that pass arguments through to the `requests` methods, accepting URL paths relative to the API root and including the necessary authorization. These are invoked inside the collection and entity action methods, and they are also available for users to call directly, whether because there are API resources we haven't wrapped in Pythonic methods yet, or because they are simpler RPC-style endpoints that just need to be hit directly.
83
+
The client object has `.get`, `.post`, etc. methods that pass arguments through to the `requests` methods, accepting URL paths relative to the API root and including the necessary authorization. These are invoked inside the collection and entity action methods, and they are also available for users to call directly, whether because there are API resources we haven't wrapped in Pythonic methods yet, or because they are simpler RPC-style endpoints that just need to be hit directly.
84
+
85
+
### Constructing classes
86
+
87
+
Classes that contain dictionary-like data should inherit from `ReadOnlyDict` (or one of its subsclasses) and classes that contain list-like data should inherit from `ReadOnlySequence` (or one of its subsclasses).
88
+
89
+
#### Classes
90
+
91
+
`ReadOnlyDict` was created to provide a non-interactive interface to the data being returned for the class. This way users can not set any values without going through the API. By extension, any method that would change the data should return a new instance with the updated data. E.g. `.update()` methods should return a instance. The same applies for `ReadOnlySequence` classes.
92
+
93
+
When retrieving objects from the server, it should be retrieved through a `@property` method. This way, the data is only retrieved when it is needed. This is especially important for list-like objects.
To avoid confusion between api exploration and internal values, all internal values should be prefixed with an underscore. This way, users can easily see what is part of the API and what is part of the internal workings of the class.
110
+
111
+
Attempt to minimize the number of locations where the same intended `path` is defined. Preferably only at `._path` (so it works with `ApiCallMixin`).
*`Context` - A convenience class that holds information that can be passed down to child classes.
128
+
* Contains the request `.session` and `.url` information for easy API calls.
129
+
* By inheriting from `Context`, it can be extended to contain more information (e.g. `ContentItemContext` adds `.content_path` and `.content_guid` to remove the requirement of passing through `content_guid` as a parameter).
130
+
* These classes help prevent an explosion of parameters being passed through the classes.
131
+
*`ContextP` - Protocol class that defines the attributes that a Context class should have.
132
+
*`ContextT` - Type variable that defines the type of the Context class.
133
+
*`ApiCallMixin` - Mixin class that provides helper methods for API calls and parsing the JSON repsonse. (e.g. `._get_api()`)
134
+
* It requires `._path: str` to be defined on the instance.
135
+
136
+
#### Ex: Content Item helper classes
137
+
138
+
These example classes show how the Entity and Context classes can be extended to provide helper classes for classes related to `ContentItem`.
139
+
140
+
*`ContentItemP` - Extends `ContextP` with context class set to `ContentItemContext`.
141
+
*`ContentItemContext` - Extends `Context` by including `content_path` and `content_guid` attributes.
142
+
*`ContentItemResourceDict` - Extends `ResourceDict` with context class set to `ContentItemContext`.
143
+
*`ContentItemActiveDict` - Extends `ActiveDict` with context class set to `ContentItemContext`.
144
+
145
+
#### Entity Classes
146
+
147
+
All entity classes are populated on initialization.
148
+
149
+
*`ReadOnlyDict` - A class that provides a read-only dictionary interface to the data.
150
+
* Immutable dictionary-like object that can be iterated over.
151
+
*`ResourceDict` - Extends `ReadOnlyDict`, but is aware of `._ctx: ContextT`.
152
+
*`ActiveDict` - Extends `ResourceDict`, but is aware of the API calls (`ApiCallMixin`) that can be made on the data.
When possible `**kwargs` should be typed with `**kwargs: Unpack[_Attrs]` where `_Attrs` is a class that defines the attributes that can be passed to the class. (Please define the attribute class within the usage class and have its name start with a `_`) By using `Unpack` and `**kwargs`, it allows for future new/conflicting parameters can be type ignored by the caller, but they will be sent through in the implementation.
178
+
179
+
Example:
180
+
181
+
```python
182
+
classAssociation(ResourceDict):
183
+
class_Attrs(TypedDict, total=False):
184
+
app_guid: str
185
+
"""The unique identifier of the content item."""
186
+
oauth_integration_guid: str
187
+
"""The unique identifier of an existing OAuth integration."""
188
+
oauth_integration_name: str
189
+
"""A descriptive name that identifies the OAuth integration."""
190
+
oauth_integration_description: str
191
+
"""A brief text that describes the OAuth integration."""
192
+
oauth_integration_template: str
193
+
"""The template used to configure this OAuth integration."""
194
+
created_time: str
195
+
"""The timestamp (RFC3339) indicating when this association was created."""
*`ReadOnlySequence` - A class that provides a read-only list interface to the data.
204
+
* Immutable list-like object that can be iterated over.
205
+
*`ResourceSequence` - Extends `ReadOnlySequence`, but is aware of `._ctx: ContextT`.
206
+
* Wants data to immediately exist in the class.
207
+
*`ActiveSequence` - Extends `ResourceSequence`, but is aware of the API calls that can be made on the data. It requires `._path`
208
+
* Requires `._create_instance(path: str, **kwars: Any) -> ResourceDictT` method to be implemented.
209
+
* During initialization, if the data is not provided, it will be fetched from the API. (...unless `get_data=False` is passed as a parameter)
210
+
*`ActiveFinderSequence` - Extends `ActiveSequence` with `.find()` and `.find_by()` methods.
211
+
212
+
For Collections classes, if no data is to be maintained, the class should inherit from `ContextP[CONTEXT_CLASS]`. This will help pass through the `._ctx` to children objects. If API calls are needed, it can also inherit from `ApiCallMixin` to get access to its conveniece methods (e.g. `._get_api()` which returns a parsed json result).
213
+
214
+
215
+
When making a new class,
216
+
* Use a class to define the parameters and their types
217
+
* If attaching the type info class to the parent class, start with `_`. E.g.: `ContentItemRepository._Attrs`
218
+
* Document all attributes like normal
219
+
* When the time comes that there are multiple attribute types, we can use overloads with full documentation and unpacking of type info class for each overload method.
220
+
* Inherit from `ApiDictEndpoint` or `ApiListEndpoint` as needed
221
+
* Init signature should be `def __init__(self, ctx: Context, path: str, /, **attrs: Jsonifiable) -> None:`
Copy file name to clipboardExpand all lines: src/posit/connect/_active.py
+9-59Lines changed: 9 additions & 59 deletions
Original file line number
Diff line number
Diff line change
@@ -1,3 +1,9 @@
1
+
# ################################
2
+
# Design Notes
3
+
#
4
+
# Please see the design notes in `src/posit/connect/README.md` for example usages.
5
+
# ################################
6
+
1
7
from __future__ importannotations
2
8
3
9
importposixpath
@@ -19,27 +25,6 @@
19
25
from ._jsonimportJsonifiable, JsonifiableList, ResponseAttrs
20
26
from ._types_contextimportContextT
21
27
22
-
# Design Notes:
23
-
# * Perform API calls on property retrieval. e.g. `my_content.repository`
24
-
# * Dictionary endpoints: Retrieve all attributes during init unless provided
25
-
# * List endpoints: Do not retrieve until `.fetch()` is called directly. Avoids cache invalidation issues.
26
-
# * While slower, all ApiListEndpoint helper methods should `.fetch()` on demand.
27
-
# * Only expose methods needed for `ReadOnlyDict`.
28
-
# * Ex: When inheriting from `dict`, we'd need to shut down `update`, `pop`, etc.
29
-
# * Use `ApiContextProtocol` to ensure that the class has the necessary attributes for API calls.
30
-
# * Inherit from `ApiCallMixin` to add all helper methods for API calls.
31
-
# * Classes should write the `path` only once within its init method.
32
-
# * Through regular interactions, the path should only be written once.
33
-
34
-
# When making a new class,
35
-
# * Use a class to define the parameters and their types
36
-
# * If attaching the type info class to the parent class, start with `_`. E.g.: `ContentItemRepository._Attrs`
37
-
# * Document all attributes like normal
38
-
# * When the time comes that there are multiple attribute types, we can use overloads with full documentation and unpacking of type info class for each overload method.
39
-
# * Inherit from `ApiDictEndpoint` or `ApiListEndpoint` as needed
40
-
# * Init signature should be `def __init__(self, ctx: Context, path: str, /, **attrs: Jsonifiable) -> None:`
0 commit comments