diff --git a/.gitignore b/.gitignore
index 1241897..d722496 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,9 @@
/.quarto/
_site/
.Rproj.user
+
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
diff --git a/extensions/sdk-assistant/.gitignore b/extensions/sdk-assistant/.gitignore
new file mode 100644
index 0000000..e36a212
--- /dev/null
+++ b/extensions/sdk-assistant/.gitignore
@@ -0,0 +1,4 @@
+chatlas/
+rsconnect-python/
+_swagger.json
+_swagger_prompt.md
diff --git a/extensions/sdk-assistant/Makefile b/extensions/sdk-assistant/Makefile
new file mode 100644
index 0000000..f28a826
--- /dev/null
+++ b/extensions/sdk-assistant/Makefile
@@ -0,0 +1,94 @@
+PYTHON ?= $(shell command -v python || command -v python3)
+.DEFAULT_GOAL := help
+
+.PHONY=FORCE
+FORCE:
+
+all: _prompt.xml manifest.json FORCE ## [py] Lazily update the prompt and manifest
+
+install: ensure-uv FORCE ## [py] Install the assistant
+ uv pip install --python 3.12 -r requirements.txt
+
+PORT=7000
+AUTORELOAD_PORT=7001
+shiny: _prompt.xml FORCE ## [py] Run the shiny app
+ @$(MAKE) install 1>/dev/null
+ uv run --python 3.12 python -m \
+ shiny run \
+ --port $(PORT) \
+ --reload --autoreload-port $(AUTORELOAD_PORT) \
+ --launch-browser \
+ app.py
+
+app.py:
+requirements.txt:
+manifest.json: app.py requirements.txt _prompt.xml
+ @$(MAKE) manifest
+manifest: ensure-uv FORCE ## [py] Write the manifest file for GitHub
+ uv run --python 3.12 \
+ --with "rsconnect-python >= 1.21.0" \
+ rsconnect write-manifest shiny \
+ -x "_swagger_prompt.md" \
+ -x "custom-prompt-instructions.md" \
+ -x "uv_*.py" \
+ -x "requirements.txt" \
+ -x "Makefile" \
+ -x "README.md" \
+ -x ".DS_Store" \
+ -x "repomix.config.json" \
+ -x ".gitignore" \
+ -x "chatlas/*" \
+ -x "_swagger.json" \
+ --overwrite \
+ .
+
+prompt: ensure-uv FORCE ## [py] Update the assistant's system prompt
+ uv run --python 3.12 uv_update_prompt.py
+
+test: ensure-uv FORCE ## [py] Test the assistant locally
+ uv run --python 3.12 uv_test_chat.py
+
+_prompt.xml: custom-prompt-instructions.md _swagger_prompt.md
+ @$(MAKE) prompt
+
+_swagger_prompt.md:
+ @$(MAKE) swagger
+
+swagger: ensure-uv FORCE ## [py] Update the Swagger file
+ uv run uv_update_swagger.py
+
+deploy: ensure-uv FORCE ## [py] Deploy the assistant
+ uv run --python 3.12 \
+ --with "rsconnect-python >= 1.21.0" \
+ rsconnect deploy shiny \
+ --server https://connect.posit.it/ \
+ --app-id 21ac1399-b840-4356-a35c-bc37d10ef1d8 \
+ .
+
+# Do not add a dep on `ensure-uv` to avoid recursive depencencies
+.venv:
+ uv venv
+ensure-uv: FORCE
+ @if ! command -v uv >/dev/null; then \
+ $(PYTHON) -m ensurepip && $(PYTHON) -m pip install "uv >= 0.5.22"; \
+ fi
+ @# Install virtual environment (before calling `uv pip install ...`)
+ @$(MAKE) .venv 1>/dev/null
+ @# Be sure recent uv is installed
+ @uv pip install "uv >= 0.4.27" --quiet
+
+help: FORCE ## Show help messages for make targets
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; { \
+ printf "\033[32m%-18s\033[0m", $$1; \
+ if ($$2 ~ /^\[docs\]/) { \
+ printf "\033[34m[docs]\033[0m%s\n", substr($$2, 7); \
+ } else if ($$2 ~ /^\[py\]/) { \
+ printf " \033[33m[py]\033[0m%s\n", substr($$2, 5); \
+ } else if ($$2 ~ /^\[ext\]/) { \
+ printf " \033[35m[ext]\033[0m%s\n", substr($$2, 6); \
+ } else if ($$2 ~ /^\[r\]/) { \
+ printf " \033[31m[r]\033[0m%s\n", substr($$2, 4); \
+ } else { \
+ printf " %s\n", $$2; \
+ } \
+ }'
diff --git a/extensions/sdk-assistant/README.md b/extensions/sdk-assistant/README.md
new file mode 100644
index 0000000..6a959f3
--- /dev/null
+++ b/extensions/sdk-assistant/README.md
@@ -0,0 +1,58 @@
+# Posit Connect SDK Assistant
+
+![SDK Assistant](sdk-assistant.gif)
+
+This is a tool to help developers understand how to use the [Posit Connect SDK python package](https://github.com/posit-dev/posit-sdk-py).
+
+Currently, it does not execute any code.
+
+## Usage
+
+To run the assistant, simply run the following command:
+
+```bash
+make shiny
+```
+
+## LLM
+
+The assistant uses AWS Bedrock to run the Anthropic model. The model is a Claude model that is trained on the Posit Connect SDK. The model is trained on the custom prompt and the Posit Connect SDK documentation and function signatures.
+
+**Requirements:**
+* Model must be enabled.
+ * ![Access Granted](readme_access_granted.png)
+ * You must have access to the model. ("Access Granted" in green in image)
+ * The model must be enabled in the region you are running the assistant. This app uses `us-east-1`. ("N. Virginia" in image)
+* Model ID must be the "Cross-region Interference" > "Inference profile ID" value. Ex: `"us.anthropic.claude-3-sonnet-20240229-v1:0"`
+* For local development, you must be logged into the AWS CLI within your terminal's session.
+ * To log into the AWS Browser Console console within the browser, call `aws-console` in a termainal with an active AWS session. Install `aws-console` by running `brew install aws-console`.
+
+## Development
+
+To update the custom prompt, edit the `custom-prompt-instructions.md` file.
+
+To compile the whole prompt, run `make prompt`. This will extract the typings from [posit-sdk](https://github.com/posit-dev/posit-sdk-py), download the latest [Connect swagger json file](https://docs.posit.co/connect/api/swagger.json), and compile the typings/swagger/custom-prompt into a single file: `_prompt.xml`.
+
+`_prompt.xml` is an output file from `repomix`. [Repomix](https://github.com/yamadashy/repomix) outputs the content in XML format which is preferred by the Claude model.
+
+
+### Future
+
+Possible default prompts:
+* [Current default prompt] What are the pieces of Posit connect and how do they fit together?
+* Can you create a sequence diagram for the typical workflow?
+
+
+### Possible TODOs
+
+* Provide common workflow examples for different User types:
+ * How to publish content (publisher)
+ * How to add a user to a group (admin)
+
+### Deployment
+
+To deploy the assistant, run the following command:
+
+```bash
+make deploy
+```
diff --git a/extensions/sdk-assistant/_prompt.xml b/extensions/sdk-assistant/_prompt.xml
new file mode 100644
index 0000000..97c6cdd
--- /dev/null
+++ b/extensions/sdk-assistant/_prompt.xml
@@ -0,0 +1,5378 @@
+This file is a merged representation of the entire codebase, combining all repository files into a single document.
+Generated by Repomix on: 2025-01-28T17:21:45.629Z
+
+
+This section contains a summary of this file.
+
+
+This file contains a packed representation of the entire repository's contents.
+It is designed to be easily consumable by AI systems for analysis, code review,
+or other automated processes.
+
+
+
+The content is organized as follows:
+1. This summary section
+2. Repository information
+3. Directory structure
+4. Repository files, each consisting of:
+ - File path as an attribute
+ - Full contents of the file
+
+
+
+- This file should be treated as read-only. Any changes should be made to the
+ original repository files, not this packed version.
+- When processing this file, use the file path to distinguish
+ between different files in the repository.
+- Be aware that this file may contain sensitive information. Handle it with
+ the same level of security as you would the original repository.
+
+- Pay special attention to the Repository Instruction. These contain important context and guidelines specific to this project.
+
+
+
+- Some files may have been excluded based on .gitignore rules and Repomix's
+ configuration.
+- Binary files are not included in this packed representation. Please refer to
+ the Repository Structure section for a complete list of file paths, including
+ binary files.
+
+
+
+
+
+
+
+
+
+connect/
+ metrics/
+ __init__.pyi
+ metrics.pyi
+ shiny_usage.pyi
+ usage.pyi
+ visits.pyi
+ oauth/
+ associations.pyi
+ integrations.pyi
+ oauth.pyi
+ sessions.pyi
+ __init__.pyi
+ _utils.pyi
+ auth.pyi
+ bundles.pyi
+ client.pyi
+ config.pyi
+ content.pyi
+ context.pyi
+ cursors.pyi
+ env.pyi
+ environments.pyi
+ errors.pyi
+ groups.pyi
+ hooks.pyi
+ jobs.pyi
+ me.pyi
+ packages.pyi
+ paginator.pyi
+ permissions.pyi
+ repository.pyi
+ resources.pyi
+ system.pyi
+ tags.pyi
+ tasks.pyi
+ urls.pyi
+ users.pyi
+ vanities.pyi
+ variants.pyi
+__init__.pyi
+_version.pyi
+
+
+
+This section contains the contents of the repository's files.
+
+
+from .metrics import Metrics as Metrics
+
+"""Metric resources."""
+
+
+
+from .. import resources
+from .usage import Usage
+
+"""Metric resources."""
+class Metrics(resources.Resources):
+ """Metrics resource.
+
+ Attributes
+ ----------
+ usage: Usage
+ Usage resource.
+ """
+ @property
+ def usage(self) -> Usage:
+ ...
+
+
+
+from typing_extensions import List, overload
+from ..resources import BaseResource, Resources
+
+class ShinyUsageEvent(BaseResource):
+ @property
+ def content_guid(self) -> str:
+ """The associated unique content identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def user_guid(self) -> str:
+ """The associated unique user identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def started(self) -> str:
+ """The started timestamp.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def ended(self) -> str:
+ """The ended timestamp.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def data_version(self) -> int:
+ """The data version.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+
+
+class ShinyUsage(Resources):
+ @overload
+ def find(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> List[ShinyUsageEvent]:
+ """Find usage.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ List[ShinyUsageEvent]
+ """
+ ...
+
+ @overload
+ def find(self, **kwargs) -> List[ShinyUsageEvent]:
+ """Find usage.
+
+ Returns
+ -------
+ List[ShinyUsageEvent]
+ """
+ ...
+
+ def find(self, **kwargs) -> List[ShinyUsageEvent]:
+ """Find usage.
+
+ Returns
+ -------
+ List[ShinyUsageEvent]
+ """
+ ...
+
+ @overload
+ def find_one(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> ShinyUsageEvent | None:
+ """Find a usage event.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ ShinyUsageEvent | None
+ """
+ ...
+
+ @overload
+ def find_one(self, **kwargs) -> ShinyUsageEvent | None:
+ """Find a usage event.
+
+ Returns
+ -------
+ ShinyUsageEvent | None
+ """
+ ...
+
+ def find_one(self, **kwargs) -> ShinyUsageEvent | None:
+ """Find a usage event.
+
+ Returns
+ -------
+ ShinyUsageEvent | None
+ """
+ ...
+
+
+
+def rename_params(params: dict) -> dict:
+ """Rename params from the internal to the external signature.
+
+ The API accepts `from` as a querystring parameter. Since `from` is a reserved word in Python, the SDK uses the name `start` instead. The querystring parameter `to` takes the same form for consistency.
+
+ Parameters
+ ----------
+ params : dict
+
+ Returns
+ -------
+ dict
+ """
+ ...
+
+
+
+from typing_extensions import List, overload
+from .. import resources
+from . import shiny_usage, visits
+
+"""Usage resources."""
+class UsageEvent(resources.BaseResource):
+ @staticmethod
+ def from_event(event: visits.VisitEvent | shiny_usage.ShinyUsageEvent) -> UsageEvent:
+ ...
+
+ @staticmethod
+ def from_visit_event(event: visits.VisitEvent) -> UsageEvent:
+ ...
+
+ @staticmethod
+ def from_shiny_usage_event(event: shiny_usage.ShinyUsageEvent) -> UsageEvent:
+ ...
+
+ @property
+ def content_guid(self) -> str:
+ """The associated unique content identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def user_guid(self) -> str:
+ """The associated unique user identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def variant_key(self) -> str | None:
+ """The variant key associated with the visit.
+
+ Returns
+ -------
+ str | None
+ The variant key, or None if the associated content type is static.
+ """
+ ...
+
+ @property
+ def rendering_id(self) -> int | None:
+ """The render id associated with the visit.
+
+ Returns
+ -------
+ int | None
+ The render id, or None if the associated content type is static.
+ """
+ ...
+
+ @property
+ def bundle_id(self) -> int | None:
+ """The bundle id associated with the visit.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ @property
+ def started(self) -> str:
+ """The visit timestamp.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def ended(self) -> str:
+ """The visit timestamp.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def data_version(self) -> int:
+ """The data version.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ @property
+ def path(self) -> str | None:
+ """The path requested by the user.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+
+
+class Usage(resources.Resources):
+ """Usage resource."""
+ @overload
+ def find(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> List[UsageEvent]:
+ """Find view events.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ List[UsageEvent]
+ """
+ ...
+
+ @overload
+ def find(self, **kwargs) -> List[UsageEvent]:
+ """Find view events.
+
+ Returns
+ -------
+ List[UsageEvent]
+ """
+ ...
+
+ def find(self, **kwargs) -> List[UsageEvent]:
+ """Find view events.
+
+ Returns
+ -------
+ List[UsageEvent]
+ """
+ ...
+
+ @overload
+ def find_one(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> UsageEvent | None:
+ """Find a view event.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ Visit | None
+ """
+ ...
+
+ @overload
+ def find_one(self, **kwargs) -> UsageEvent | None:
+ """Find a view event.
+
+ Returns
+ -------
+ Visit | None
+ """
+ ...
+
+ def find_one(self, **kwargs) -> UsageEvent | None:
+ """Find a view event.
+
+ Returns
+ -------
+ UsageEvent | None
+ """
+ ...
+
+
+
+from typing_extensions import List, overload
+from ..resources import BaseResource, Resources
+
+class VisitEvent(BaseResource):
+ @property
+ def content_guid(self) -> str:
+ """The associated unique content identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def user_guid(self) -> str:
+ """The associated unique user identifier.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def rendering_id(self) -> int | None:
+ """The render id associated with the visit.
+
+ Returns
+ -------
+ int | None
+ The render id, or None if the associated content type is static.
+ """
+ ...
+
+ @property
+ def bundle_id(self) -> int:
+ """The bundle id associated with the visit.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ @property
+ def variant_key(self) -> str | None:
+ """The variant key associated with the visit.
+
+ Returns
+ -------
+ str | None
+ The variant key, or None if the associated content type is static.
+ """
+ ...
+
+ @property
+ def time(self) -> str:
+ """The visit timestamp.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+ @property
+ def data_version(self) -> int:
+ """The data version.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ @property
+ def path(self) -> str:
+ """The path requested by the user.
+
+ Returns
+ -------
+ str
+ """
+ ...
+
+
+
+class Visits(Resources):
+ @overload
+ def find(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> List[VisitEvent]:
+ """Find visits.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ List[Visit]
+ """
+ ...
+
+ @overload
+ def find(self, **kwargs) -> List[VisitEvent]:
+ """Find visits.
+
+ Returns
+ -------
+ List[Visit]
+ """
+ ...
+
+ def find(self, **kwargs) -> List[VisitEvent]:
+ """Find visits.
+
+ Returns
+ -------
+ List[Visit]
+ """
+ ...
+
+ @overload
+ def find_one(self, *, content_guid: str = ..., min_data_version: int = ..., start: str = ..., end: str = ...) -> VisitEvent | None:
+ """Find a visit.
+
+ Parameters
+ ----------
+ content_guid : str, optional
+ Filter by an associated unique content identifer, by default ...
+ min_data_version : int, optional
+ Filter by a minimum data version, by default ...
+ start : str, optional
+ Filter by the start time, by default ...
+ end : str, optional
+ Filter by the end time, by default ...
+
+ Returns
+ -------
+ Visit | None
+ """
+ ...
+
+ @overload
+ def find_one(self, **kwargs) -> VisitEvent | None:
+ """Find a visit.
+
+ Returns
+ -------
+ Visit | None
+ """
+ ...
+
+ def find_one(self, **kwargs) -> VisitEvent | None:
+ """Find a visit.
+
+ Returns
+ -------
+ Visit | None
+ """
+ ...
+
+
+
+def rename_params(params: dict) -> dict:
+ """Rename params from the internal to the external signature.
+
+ The API accepts `from` as a querystring parameter. Since `from` is a reserved word in Python, the SDK uses the name `start` instead. The querystring parameter `to` takes the same form for consistency.
+
+ Parameters
+ ----------
+ params : dict
+
+ Returns
+ -------
+ dict
+ """
+ ...
+
+
+
+from typing_extensions import List
+from ..context import Context
+from ..resources import BaseResource, Resources
+
+"""OAuth association resources."""
+class Association(BaseResource):
+ ...
+
+
+class IntegrationAssociations(Resources):
+ """IntegrationAssociations resource."""
+ def __init__(self, ctx: Context, integration_guid: str) -> None:
+ ...
+
+ def find(self) -> List[Association]:
+ """Find OAuth associations.
+
+ Returns
+ -------
+ List[Association]
+ """
+ ...
+
+
+
+class ContentItemAssociations(Resources):
+ """ContentItemAssociations resource."""
+ def __init__(self, ctx, content_guid: str) -> None:
+ ...
+
+ def find(self) -> List[Association]:
+ """Find OAuth associations.
+
+ Returns
+ -------
+ List[Association]
+ """
+ ...
+
+ def delete(self) -> None:
+ """Delete integration associations."""
+ ...
+
+ def update(self, integration_guid: str) -> None:
+ """Set integration associations."""
+ ...
+
+
+
+from typing_extensions import List, Optional, overload
+from ..resources import BaseResource, Resources
+from .associations import IntegrationAssociations
+
+"""OAuth integration resources."""
+class Integration(BaseResource):
+ """OAuth integration resource."""
+ @property
+ def associations(self) -> IntegrationAssociations:
+ ...
+
+ def delete(self) -> None:
+ """Delete the OAuth integration."""
+ ...
+
+ @overload
+ def update(self, *args, name: str = ..., description: str = ..., config: dict = ..., **kwargs) -> None:
+ """Update the OAuth integration.
+
+ Parameters
+ ----------
+ name: str, optional
+ description: str, optional
+ config: dict, optional
+ """
+ ...
+
+ @overload
+ def update(self, *args, **kwargs) -> None:
+ """Update the OAuth integration."""
+ ...
+
+ def update(self, *args, **kwargs) -> None:
+ """Update the OAuth integration."""
+ ...
+
+
+
+class Integrations(Resources):
+ """Integrations resource."""
+ @overload
+ def create(self, *, name: str, description: Optional[str], template: str, config: dict) -> Integration:
+ """Create an OAuth integration.
+
+ Parameters
+ ----------
+ name : str
+ description : Optional[str]
+ template : str
+ config : dict
+
+ Returns
+ -------
+ Integration
+ """
+ ...
+
+ @overload
+ def create(self, **kwargs) -> Integration:
+ """Create an OAuth integration.
+
+ Returns
+ -------
+ Integration
+ """
+ ...
+
+ def create(self, **kwargs) -> Integration:
+ """Create an OAuth integration.
+
+ Parameters
+ ----------
+ name : str
+ description : Optional[str]
+ template : str
+ config : dict
+
+ Returns
+ -------
+ Integration
+ """
+ ...
+
+ def find(self) -> List[Integration]:
+ """Find OAuth integrations.
+
+ Returns
+ -------
+ List[Integration]
+ """
+ ...
+
+ def get(self, guid: str) -> Integration:
+ """Get an OAuth integration.
+
+ Parameters
+ ----------
+ guid: str
+
+ Returns
+ -------
+ Integration
+ """
+ ...
+
+
+
+from typing_extensions import Optional, TYPE_CHECKING, TypedDict
+from ..resources import Resources
+from ..context import Context
+
+if TYPE_CHECKING:
+ ...
+GRANT_TYPE = ...
+USER_SESSION_TOKEN_TYPE = ...
+CONTENT_SESSION_TOKEN_TYPE = ...
+API_KEY_TOKEN_TYPE = ...
+class OAuth(Resources):
+ def __init__(self, ctx: Context, api_key: str) -> None:
+ ...
+
+ @property
+ def integrations(self): # -> Integrations:
+ ...
+
+ @property
+ def sessions(self): # -> Sessions:
+ ...
+
+ def get_credentials(self, user_session_token: Optional[str] = ..., requested_token_type: Optional[str] = ...) -> Credentials:
+ """Perform an oauth credential exchange with a user-session-token."""
+ ...
+
+ def get_content_credentials(self, content_session_token: Optional[str] = ...) -> Credentials:
+ """Perform an oauth credential exchange with a content-session-token."""
+ ...
+
+
+
+class Credentials(TypedDict, total=False):
+ access_token: str
+ issued_token_type: str
+ token_type: str
+ ...
+
+
+
+from typing_extensions import List, Optional, overload
+from ..resources import BaseResource, Resources
+
+"""OAuth session resources."""
+class Session(BaseResource):
+ """OAuth session resource."""
+ def delete(self) -> None:
+ ...
+
+
+
+class Sessions(Resources):
+ @overload
+ def find(self, *, all: Optional[bool] = ...) -> List[Session]:
+ ...
+
+ @overload
+ def find(self, **kwargs) -> List[Session]:
+ ...
+
+ def find(self, **kwargs) -> List[Session]:
+ ...
+
+ def get(self, guid: str) -> Session:
+ """Get an OAuth session.
+
+ Parameters
+ ----------
+ guid: str
+
+ Returns
+ -------
+ Session
+ """
+ ...
+
+
+
+from .client import Client as Client
+
+
+
+from typing_extensions import Any
+
+def update_dict_values(obj: dict[str, Any], /, **kwargs: Any) -> None:
+ """
+ Update the values of a dictionary.
+
+ This helper method exists as a workaround for the `dict.update` method. Sometimes, `super()` does not return the `dict` class and. If `super().update(**kwargs)` is called unintended behavior will occur.
+
+ Therefore, this helper method exists to update the `dict`'s values.
+
+ Parameters
+ ----------
+ obj : dict[str, Any]
+ The object to update.
+ kwargs : Any
+ The key-value pairs to update the object with.
+
+ See Also
+ --------
+ * https://github.com/posit-dev/posit-sdk-py/pull/366#discussion_r1887845267
+ """
+ ...
+
+def is_local() -> bool:
+ """Returns true if called from a piece of content running on a Connect server.
+
+ The connect server will always set the environment variable `RSTUDIO_PRODUCT=CONNECT`.
+ We can use this environment variable to determine if the content is running locally
+ or on a Connect server.
+ """
+ ...
+
+
+
+from requests import PreparedRequest
+from requests.auth import AuthBase
+from .config import Config
+
+"""Provides authentication functionality."""
+class Auth(AuthBase):
+ """Handles authentication for API requests."""
+ def __init__(self, config: Config) -> None:
+ ...
+
+ def __call__(self, r: PreparedRequest) -> PreparedRequest:
+ """Add authorization header to the request."""
+ ...
+
+
+
+import io
+from typing_extensions import List, TYPE_CHECKING
+from . import resources, tasks
+from .context import Context
+
+"""Bundle resources."""
+if TYPE_CHECKING:
+ ...
+class BundleMetadata(resources.BaseResource):
+ ...
+
+
+class Bundle(resources.BaseResource):
+ @property
+ def metadata(self) -> BundleMetadata:
+ ...
+
+ def delete(self) -> None:
+ """Delete the bundle."""
+ ...
+
+ def deploy(self) -> tasks.Task:
+ """Deploy the bundle.
+
+ Spawns an asynchronous task, which activates the bundle.
+
+ Returns
+ -------
+ tasks.Task
+ The task for the deployment.
+
+ Examples
+ --------
+ >>> task = bundle.deploy()
+ >>> task.wait_for()
+ None
+ """
+ ...
+
+ def download(self, output: io.BufferedWriter | str) -> None:
+ """Download a bundle.
+
+ Download a bundle to a file or memory.
+
+ Parameters
+ ----------
+ output : io.BufferedWriter or str
+ An io.BufferedWriter instance or a str representing a relative or absolute path.
+
+ Raises
+ ------
+ TypeError
+ If the output is not of type `io.BufferedWriter` or `str`.
+
+ Examples
+ --------
+ Write to a file.
+ >>> bundle.download("bundle.tar.gz")
+ None
+
+ Write to an io.BufferedWriter.
+ >>> with open('bundle.tar.gz', 'wb') as file:
+ >>> bundle.download(file)
+ None
+ """
+ ...
+
+
+
+class Bundles(resources.Resources):
+ """Bundles resource.
+
+ Parameters
+ ----------
+ config : config.Config
+ Configuration object.
+ session : requests.Session
+ HTTP session object.
+ content_guid : str
+ Content GUID associated with the bundles.
+
+ Attributes
+ ----------
+ content_guid: str
+ Content GUID associated with the bundles.
+ """
+ def __init__(self, ctx: Context, content_guid: str) -> None:
+ ...
+
+ def create(self, archive: io.BufferedReader | bytes | str) -> Bundle:
+ """
+ Create a bundle.
+
+ Create a bundle from a file or memory.
+
+ Parameters
+ ----------
+ archive : io.BufferedReader, bytes, or str
+ Archive for bundle creation. A 'str' type assumes a relative or absolute filepath.
+
+ Returns
+ -------
+ Bundle
+ The created bundle.
+
+ Raises
+ ------
+ TypeError
+ If the input is not of type `io.BufferedReader`, `bytes`, or `str`.
+
+ Examples
+ --------
+ Create a bundle from io.BufferedReader
+ >>> with open('bundle.tar.gz', 'rb') as file:
+ >>> bundle.create(file)
+ None
+
+ Create a bundle from bytes.
+ >>> with open('bundle.tar.gz', 'rb') as file:
+ >>> data: bytes = file.read()
+ >>> bundle.create(data)
+ None
+
+ Create a bundle from pathname.
+ >>> bundle.create("bundle.tar.gz")
+ None
+ """
+ ...
+
+ def find(self) -> List[Bundle]:
+ """Find all bundles.
+
+ Returns
+ -------
+ list of Bundle
+ List of all found bundles.
+ """
+ ...
+
+ def find_one(self) -> Bundle | None:
+ """Find a bundle.
+
+ Returns
+ -------
+ Bundle | None
+ The first found bundle | None if no bundles are found.
+ """
+ ...
+
+ def get(self, uid: str) -> Bundle:
+ """Get a bundle.
+
+ Parameters
+ ----------
+ uid : str
+ Identifier of the bundle to retrieve.
+
+ Returns
+ -------
+ Bundle
+ The bundle with the specified ID.
+ """
+ ...
+
+
+
+from requests import Response
+from typing_extensions import TYPE_CHECKING, overload
+from .content import Content
+from .context import ContextManager, requires
+from .groups import Groups
+from .metrics.metrics import Metrics
+from .oauth.oauth import OAuth
+from .system import System
+from .tags import Tags
+from .tasks import Tasks
+from .users import User, Users
+from .vanities import Vanities
+from .environments import Environments
+from .packages import Packages
+
+"""Client connection for Posit Connect."""
+if TYPE_CHECKING:
+ ...
+class Client(ContextManager):
+ """
+ Client connection for Posit Connect.
+
+ This class provides an interface to interact with the Posit Connect API,
+ allowing for authentication, resource management, and data retrieval.
+
+ Parameters
+ ----------
+ api_key : str, optional
+ API key for authentication
+ url : str, optional
+ Sever API URL
+
+ Attributes
+ ----------
+ content: Content
+ Content resource.
+ environments: Environments
+ Environments resource.
+ groups: Groups
+ Groups resource.
+ me: User
+ Current user resource.
+ metrics: Metrics
+ Metrics resource.
+ oauth: OAuth
+ OAuth resource.
+ packages: Packages
+ Packages resource.
+ system: System
+ System resource.
+ tags: Tags
+ Tags resource.
+ tasks: Tasks
+ Tasks resource.
+ users: Users
+ Users resource.
+ vanities: Vanities
+ Vanities resource.
+ version: str
+ The server version.
+ """
+ @overload
+ def __init__(self) -> None:
+ """Initialize a Client instance.
+
+ Creates a client instance using credentials read from the environment.
+
+ Environment Variables
+ ---------------------
+ CONNECT_SERVER - The Connect server URL.
+ CONNECT_API_KEY - The API key credential for client authentication.
+
+ Examples
+ --------
+ Client()
+ """
+ ...
+
+ @overload
+ def __init__(self, url: str) -> None:
+ """Initialize a Client instance.
+
+ Creates a client instance using a provided URL and API key credential read from the environment.
+
+ Environment Variables
+ ---------------------
+ CONNECT_API_KEY - The API key credential for client authentication.
+
+ Parameters
+ ----------
+ url : str
+ The Connect server URL.
+
+ Examples
+ --------
+ Client("https://connect.example.com)
+ """
+ ...
+
+ @overload
+ def __init__(self, url: str, api_key: str) -> None:
+ """Initialize a Client instance.
+
+ Parameters
+ ----------
+ url : str
+ The Connect server URL.
+ api_key : str
+ The API key credential for client authentication.
+
+ Examples
+ --------
+ >>> Client("https://connect.example.com", abcdefghijklmnopqrstuvwxyz012345")
+ """
+ ...
+
+ def __init__(self, *args, **kwargs) -> None:
+ """Initialize a Client instance.
+
+ Environment Variables
+ ---------------------
+ CONNECT_SERVER - The Connect server URL.
+ CONNECT_API_KEY - The API key credential for client authentication.
+
+ Parameters
+ ----------
+ *args
+ Variable length argument list. Can accept:
+ - (url: str)
+ url: str
+ The Connect server URL.
+ - (url: str, api_key: str)
+ url: str
+ The Connect server URL.
+ api_key: str
+ The API key credential for client authentication.
+
+ **kwargs
+ Keyword arguments. Can include 'url' and 'api_key'.
+
+ Examples
+ --------
+ >>> Client()
+ >>> Client("https://connect.example.com")
+ >>> Client("https://connect.example.com", abcdefghijklmnopqrstuvwxyz012345")
+ >>> Client(api_key=""abcdefghijklmnopqrstuvwxyz012345", url="https://connect.example.com")
+ """
+ ...
+
+ @requires("2025.01.0")
+ def with_user_session_token(self, token: str) -> Client:
+ """Create a new Client scoped to the user specified in the user session token.
+
+ Create a new Client instance from a user session token exchange for an api key scoped to the
+ user specified in the token (the user viewing your app). If running your application locally,
+ a user session token will not exist, which will cause this method to result in an error needing
+ to be handled in your application.
+
+ Parameters
+ ----------
+ token : str
+ The user session token.
+
+ Returns
+ -------
+ Client
+ A new Client instance authenticated with an API key exchanged for the user session token.
+
+ Examples
+ --------
+ >>> from posit.connect import Client
+ >>> client = Client().with_user_session_token("my-user-session-token")
+ """
+ ...
+
+ @property
+ def content(self) -> Content:
+ """
+ The content resource interface.
+
+ Returns
+ -------
+ Content
+ The content resource instance.
+ """
+ ...
+
+ @property
+ @requires(version="2023.05.0")
+ def environments(self) -> Environments:
+ ...
+
+ @property
+ def groups(self) -> Groups:
+ """The groups resource interface.
+
+ Returns
+ -------
+ Groups
+ The groups resource interface.
+ """
+ ...
+
+ @property
+ def me(self) -> User:
+ """
+ The connected user.
+
+ Returns
+ -------
+ User
+ The currently authenticated user.
+ """
+ ...
+
+ @property
+ def metrics(self) -> Metrics:
+ """
+ The Metrics API interface.
+
+ The Metrics API is designed for capturing, retrieving, and managing
+ quantitative measurements of Connect interactions. It is commonly used
+ for monitoring and analyzing system performance, user behavior, and
+ business processes. This API facilitates real-time data collection and
+ accessibility, enabling organizations to make informed decisions based
+ on key performance indicators (KPIs).
+
+ Returns
+ -------
+ Metrics
+ The metrics API instance.
+
+ Examples
+ --------
+ >>> from posit import connect
+ >>> client = connect.Client()
+ >>> content_guid = "2243770d-ace0-4782-87f9-fe2aeca14fc8"
+ >>> events = client.metrics.usage.find(content_guid=content_guid)
+ >>> len(events)
+ 24
+ """
+ ...
+
+ @property
+ @requires(version="2024.08.0")
+ def oauth(self) -> OAuth:
+ """
+ The OAuth API interface.
+
+ Returns
+ -------
+ OAuth
+ The oauth API instance.
+ """
+ ...
+
+ @property
+ @requires(version="2024.11.0")
+ def packages(self) -> Packages:
+ ...
+
+ @property
+ def system(self) -> System:
+ ...
+
+ @property
+ def tags(self) -> Tags:
+ """
+ The tags resource interface.
+
+ Returns
+ -------
+ Tags
+ The tags resource instance.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+
+ tags = client.tags.find()
+ ```
+ """
+ ...
+
+ @property
+ def tasks(self) -> Tasks:
+ """
+ The tasks resource interface.
+
+ Returns
+ -------
+ tasks.Tasks
+ The tasks resource instance.
+ """
+ ...
+
+ @property
+ def users(self) -> Users:
+ """
+ The users resource interface.
+
+ Returns
+ -------
+ Users
+ The users resource instance.
+ """
+ ...
+
+ @property
+ def vanities(self) -> Vanities:
+ ...
+
+ @property
+ def version(self) -> str | None:
+ """
+ The server version.
+
+ Returns
+ -------
+ str
+ The version of the Posit Connect server.
+ """
+ ...
+
+ def __del__(self): # -> None:
+ """Close the session when the Client instance is deleted."""
+ ...
+
+ def __enter__(self): # -> Self:
+ """Enter method for using the client as a context manager."""
+ ...
+
+ def __exit__(self, exc_type, exc_value, exc_tb): # -> None:
+ """
+ Close the session if it exists.
+
+ Parameters
+ ----------
+ exc_type : type
+ The type of the exception raised (if any).
+ exc_value : Exception
+ The exception instance raised (if any).
+ exc_tb : traceback
+ The traceback for the exception raised (if any).
+ """
+ ...
+
+ def request(self, method: str, path: str, **kwargs) -> Response:
+ """
+ Send an HTTP request.
+
+ A facade for [](`requests.request`) configured for the target server.
+
+ Parameters
+ ----------
+ method : str
+ The HTTP method to use for the request.
+ path : str
+ Appended to the url object attribute.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.request`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+ def get(self, path: str, **kwargs) -> Response:
+ """
+ Send a GET request.
+
+ A facade for [](`requests.get`) configured for the target server.
+
+ Parameters
+ ----------
+ path : str
+ Appended to the configured base url.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.get`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+ def post(self, path: str, **kwargs) -> Response:
+ """
+ Send a POST request.
+
+ A facade for [](`requests.post`) configured for the target server.
+
+ Parameters
+ ----------
+ path : str
+ Appended to the configured base url.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.post`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+ def put(self, path: str, **kwargs) -> Response:
+ """
+ Send a PUT request.
+
+ A facade for [](`requests.put`) configured for the target server.
+
+ Parameters
+ ----------
+ path : str
+ Appended to the configured base url.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.put`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+ def patch(self, path: str, **kwargs) -> Response:
+ """
+ Send a PATCH request.
+
+ A facade for [](`requests.patch`) configured for the target server.
+
+ Parameters
+ ----------
+ path : str
+ Appended to the configured base url.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.patch`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+ def delete(self, path: str, **kwargs) -> Response:
+ """
+ Send a DELETE request.
+
+ A facade for [](`requests.delete`) configured for the target server.
+
+ Parameters
+ ----------
+ path : str
+ Appended to the configured base url.
+ **kwargs
+ Additional keyword arguments passed to [](`requests.delete`).
+
+ Returns
+ -------
+ Response
+ A [](`requests.Response`) object.
+ """
+ ...
+
+
+
+from typing_extensions import Optional
+
+"""Client configuration."""
+class Config:
+ """Configuration object."""
+ def __init__(self, api_key: Optional[str] = ..., url: Optional[str] = ...) -> None:
+ ...
+
+
+
+from typing_extensions import Any, List, Literal, NotRequired, Optional, Required, TYPE_CHECKING, TypedDict, Unpack, overload
+from . import tasks
+from .bundles import Bundles
+from .context import Context, requires
+from .env import EnvVars
+from .oauth.associations import ContentItemAssociations
+from .permissions import Permissions
+from .repository import ContentItemRepositoryMixin
+from .resources import Active, BaseResource, Resources
+from .tags import ContentItemTags
+from .vanities import VanityMixin
+from .jobs import Jobs
+from .packages import ContentPackages
+from .tasks import Task
+
+"""Content resources."""
+if TYPE_CHECKING:
+ ...
+class ContentItemOAuth(BaseResource):
+ def __init__(self, ctx: Context, content_guid: str) -> None:
+ ...
+
+ @property
+ def associations(self) -> ContentItemAssociations:
+ ...
+
+
+
+class ContentItemOwner(BaseResource):
+ ...
+
+
+class ContentItem(Active, ContentItemRepositoryMixin, VanityMixin, BaseResource):
+ class _AttrsBase(TypedDict, total=False):
+ title: NotRequired[str]
+ description: NotRequired[str]
+ access_type: NotRequired[Literal["all", "acl", "logged_in"]]
+ connection_timeout: NotRequired[int]
+ read_timeout: NotRequired[int]
+ init_timeout: NotRequired[int]
+ idle_timeout: NotRequired[int]
+ max_processes: NotRequired[int]
+ min_processes: NotRequired[int]
+ max_conns_per_process: NotRequired[int]
+ load_factor: NotRequired[float]
+ cpu_request: NotRequired[float]
+ cpu_limit: NotRequired[float]
+ memory_request: NotRequired[int]
+ memory_limit: NotRequired[int]
+ amd_gpu_limit: NotRequired[int]
+ nvidia_gpu_limit: NotRequired[int]
+ run_as: NotRequired[str]
+ run_as_current_user: NotRequired[bool]
+ default_image_name: NotRequired[str]
+ default_r_environment_management: NotRequired[bool]
+ default_py_environment_management: NotRequired[bool]
+ service_account_name: NotRequired[str]
+ ...
+
+
+ class _AttrsNotRequired(_AttrsBase):
+ name: NotRequired[str]
+ owner_guid: NotRequired[str]
+ ...
+
+
+ class _Attrs(_AttrsBase):
+ name: Required[str]
+ owner_guid: NotRequired[str]
+ ...
+
+
+ class _AttrsCreate(_AttrsBase):
+ name: NotRequired[str]
+ ...
+
+
+ @overload
+ def __init__(self, ctx: Context, /, *, guid: str) -> None:
+ ...
+
+ @overload
+ def __init__(self, ctx: Context, /, *, guid: str, **kwargs: Unpack[ContentItem._Attrs]) -> None:
+ ...
+
+ def __init__(self, ctx: Context, /, *, guid: str, **kwargs: Unpack[ContentItem._AttrsNotRequired]) -> None:
+ ...
+
+ def __getitem__(self, key: Any) -> Any:
+ ...
+
+ @property
+ def oauth(self) -> ContentItemOAuth:
+ ...
+
+ def delete(self) -> None:
+ """Delete the content item."""
+ ...
+
+ def deploy(self) -> tasks.Task:
+ """Deploy the content.
+
+ Spawns an asynchronous task, which activates the latest bundle.
+
+ Returns
+ -------
+ tasks.Task
+ The task for the deployment.
+
+ Examples
+ --------
+ >>> task = content.deploy()
+ >>> task.wait_for()
+ None
+ """
+ ...
+
+ def render(self) -> Task:
+ """Render the content.
+
+ Submit a render request to the server for the content. After submission, the server executes an asynchronous process to render the content. This is useful when content is dependent on external information, such as a dataset.
+
+ See Also
+ --------
+ restart
+
+ Examples
+ --------
+ >>> render()
+ """
+ ...
+
+ def restart(self) -> None:
+ """Mark for restart.
+
+ Sends a restart request to the server for the content. Once submitted, the server performs an asynchronous process to restart the content. This is particularly useful when the content relies on external information loaded into application memory, such as datasets. Additionally, restarting can help clear memory leaks or reduce excessive memory usage that might build up over time.
+
+ See Also
+ --------
+ render
+
+ Examples
+ --------
+ >>> restart()
+ """
+ ...
+
+ def update(self, **attrs: Unpack[ContentItem._Attrs]) -> None:
+ """Update the content item.
+
+ Parameters
+ ----------
+ name : str
+ URL-friendly identifier. Allows alphanumeric characters, hyphens ("-"), and underscores ("_").
+ title : str, optional
+ Content title. Default is None.
+ description : str, optional
+ Content description. Default is None.
+ access_type : Literal['all', 'acl', 'logged_in'], optional
+ How content manages viewers. Default is 'acl'. Options: 'all', 'logged_in', 'acl'.
+ owner_guid : str, optional
+ The unique identifier of the user who owns this content item. Default is None.
+ connection_timeout : int, optional
+ Max seconds without data exchange. Default is None. Falls back to server setting 'Scheduler.ConnectionTimeout'.
+ read_timeout : int, optional
+ Max seconds without data received. Default is None. Falls back to server setting 'Scheduler.ReadTimeout'.
+ init_timeout : int, optional
+ Max startup time for interactive apps. Default is None. Falls back to server setting 'Scheduler.InitTimeout'.
+ idle_timeout : int, optional
+ Max idle time before process termination. Default is None. Falls back to server setting 'Scheduler.IdleTimeout'.
+ max_processes : int, optional
+ Max concurrent processes allowed. Default is None. Falls back to server setting 'Scheduler.MaxProcesses'.
+ min_processes : int, optional
+ Min concurrent processes required. Default is None. Falls back to server setting 'Scheduler.MinProcesses'.
+ max_conns_per_process : int, optional
+ Max client connections per process. Default is None. Falls back to server setting 'Scheduler.MaxConnsPerProcess'.
+ load_factor : float, optional
+ Aggressiveness in spawning new processes (0.0 - 1.0). Default is None. Falls back to server setting 'Scheduler.LoadFactor'.
+ cpu_request : float, optional
+ Min CPU units required (1 unit = 1 core). Default is None. Falls back to server setting 'Scheduler.CPURequest'.
+ cpu_limit : float, optional
+ Max CPU units allowed. Default is None. Falls back to server setting 'Scheduler.CPULimit'.
+ memory_request : int, optional
+ Min memory (bytes) required. Default is None. Falls back to server setting 'Scheduler.MemoryRequest'.
+ memory_limit : int, optional
+ Max memory (bytes) allowed. Default is None. Falls back to server setting 'Scheduler.MemoryLimit'.
+ amd_gpu_limit : int, optional
+ Number of AMD GPUs allocated. Default is None. Falls back to server setting 'Scheduler.AMDGPULimit'.
+ nvidia_gpu_limit : int, optional
+ Number of NVIDIA GPUs allocated. Default is None. Falls back to server setting 'Scheduler.NvidiaGPULimit'.
+ run_as : str, optional
+ UNIX user to execute the content. Default is None. Falls back to server setting 'Applications.RunAs'.
+ run_as_current_user : bool, optional
+ Run process as the visiting user (for app content). Default is False.
+ default_image_name : str, optional
+ Default image for execution if not defined in the bundle. Default is None.
+ default_r_environment_management : bool, optional
+ Manage R environment for the content. Default is None.
+ default_py_environment_management : bool, optional
+ Manage Python environment for the content. Default is None.
+ service_account_name : str, optional
+ Kubernetes service account name for running content. Default is None.
+
+ Returns
+ -------
+ None
+ """
+ ...
+
+ @property
+ def bundles(self) -> Bundles:
+ ...
+
+ @property
+ def environment_variables(self) -> EnvVars:
+ ...
+
+ @property
+ def permissions(self) -> Permissions:
+ ...
+
+ @property
+ def owner(self) -> dict:
+ ...
+
+ @property
+ def is_interactive(self) -> bool:
+ ...
+
+ @property
+ def is_rendered(self) -> bool:
+ ...
+
+ @property
+ def tags(self) -> ContentItemTags:
+ ...
+
+ @property
+ def jobs(self) -> Jobs:
+ ...
+
+ @property
+ @requires(version="2024.11.0")
+ def packages(self) -> ContentPackages:
+ ...
+
+
+
+class Content(Resources):
+ """Content resource.
+
+ Parameters
+ ----------
+ config : Config
+ Configuration object.
+ session : Session
+ Requests session object.
+ owner_guid : str, optional
+ Content item owner identifier. Filters results to those owned by a specific user (the default is None, which implies not filtering results on owner identifier).
+ """
+ def __init__(self, ctx: Context, *, owner_guid: str | None = ...) -> None:
+ ...
+
+ def count(self) -> int:
+ """Count the number of content items.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ def create(self, **attrs: Unpack[ContentItem._AttrsCreate]) -> ContentItem:
+ """Create content.
+
+ Parameters
+ ----------
+ name : str
+ URL-friendly identifier. Allows alphanumeric characters, hyphens ("-"), and underscores ("_").
+ title : str, optional
+ Content title. Default is None.
+ description : str, optional
+ Content description. Default is None.
+ access_type : Literal['all', 'acl', 'logged_in'], optional
+ How content manages viewers. Default is 'acl'. Options: 'all', 'logged_in', 'acl'.
+ connection_timeout : int, optional
+ Max seconds without data exchange. Default is None. Falls back to server setting 'Scheduler.ConnectionTimeout'.
+ read_timeout : int, optional
+ Max seconds without data received. Default is None. Falls back to server setting 'Scheduler.ReadTimeout'.
+ init_timeout : int, optional
+ Max startup time for interactive apps. Default is None. Falls back to server setting 'Scheduler.InitTimeout'.
+ idle_timeout : int, optional
+ Max idle time before process termination. Default is None. Falls back to server setting 'Scheduler.IdleTimeout'.
+ max_processes : int, optional
+ Max concurrent processes allowed. Default is None. Falls back to server setting 'Scheduler.MaxProcesses'.
+ min_processes : int, optional
+ Min concurrent processes required. Default is None. Falls back to server setting 'Scheduler.MinProcesses'.
+ max_conns_per_process : int, optional
+ Max client connections per process. Default is None. Falls back to server setting 'Scheduler.MaxConnsPerProcess'.
+ load_factor : float, optional
+ Aggressiveness in spawning new processes (0.0 - 1.0). Default is None. Falls back to server setting 'Scheduler.LoadFactor'.
+ cpu_request : float, optional
+ Min CPU units required (1 unit = 1 core). Default is None. Falls back to server setting 'Scheduler.CPURequest'.
+ cpu_limit : float, optional
+ Max CPU units allowed. Default is None. Falls back to server setting 'Scheduler.CPULimit'.
+ memory_request : int, optional
+ Min memory (bytes) required. Default is None. Falls back to server setting 'Scheduler.MemoryRequest'.
+ memory_limit : int, optional
+ Max memory (bytes) allowed. Default is None. Falls back to server setting 'Scheduler.MemoryLimit'.
+ amd_gpu_limit : int, optional
+ Number of AMD GPUs allocated. Default is None. Falls back to server setting 'Scheduler.AMDGPULimit'.
+ nvidia_gpu_limit : int, optional
+ Number of NVIDIA GPUs allocated. Default is None. Falls back to server setting 'Scheduler.NvidiaGPULimit'.
+ run_as : str, optional
+ UNIX user to execute the content. Default is None. Falls back to server setting 'Applications.RunAs'.
+ run_as_current_user : bool, optional
+ Run process as the visiting user (for app content). Default is False.
+ default_image_name : str, optional
+ Default image for execution if not defined in the bundle. Default is None.
+ default_r_environment_management : bool, optional
+ Manage R environment for the content. Default is None.
+ default_py_environment_management : bool, optional
+ Manage Python environment for the content. Default is None.
+ service_account_name : str, optional
+ Kubernetes service account name for running content. Default is None.
+ **attributes : Any
+ Additional attributes.
+
+ Returns
+ -------
+ ContentItem
+ """
+ ...
+
+ @overload
+ def find(self, *, name: Optional[str] = ..., owner_guid: Optional[str] = ..., include: Optional[Literal["owner", "tags", "vanity_url"] | list[Literal["owner", "tags", "vanity_url"]]] = ...) -> List[ContentItem]:
+ """Find content matching the specified criteria.
+
+ **Applies to Connect versions 2024.06.0 and later.**
+
+ Parameters
+ ----------
+ name : str, optional
+ The content name specified at creation; unique within the owner's account.
+ owner_guid : str, optional
+ The UUID of the content owner.
+ include : str or list of str, optional
+ Additional details to include in the response. Allowed values: 'owner', 'tags', 'vanity_url'.
+
+ Returns
+ -------
+ List[ContentItem]
+ List of matching content items.
+
+ Note
+ ----
+ Specifying both `name` and `owner_guid` returns at most one content item due to uniqueness.
+ """
+ ...
+
+ @overload
+ def find(self, *, name: Optional[str] = ..., owner_guid: Optional[str] = ..., include: Optional[Literal["owner", "tags"] | list[Literal["owner", "tags"]]] = ...) -> List[ContentItem]:
+ """Find content matching the specified criteria.
+
+ **Applies to Connect versions prior to 2024.06.0.**
+
+ Parameters
+ ----------
+ name : str, optional
+ The content name specified at creation; unique within the owner's account.
+ owner_guid : str, optional
+ The UUID of the content owner.
+ include : str or list of str, optional
+ Additional details to include in the response. Allowed values: 'owner', 'tags'.
+
+ Returns
+ -------
+ List[ContentItem]
+ List of matching content items.
+
+ Note
+ ----
+ Specifying both `name` and `owner_guid` returns at most one content item due to uniqueness.
+ """
+ ...
+
+ @overload
+ def find(self, include: Optional[str | list[Any]], **conditions) -> List[ContentItem]:
+ ...
+
+ def find(self, include: Optional[str | list[Any]] = ..., **conditions) -> List[ContentItem]:
+ """Find content matching the specified conditions.
+
+ Returns
+ -------
+ List[ContentItem]
+ """
+ ...
+
+ def find_by(self, **attrs: Unpack[ContentItem._AttrsNotRequired]) -> Optional[ContentItem]:
+ """Find the first content record matching the specified attributes.
+
+ There is no implied ordering so if order matters, you should find it yourself.
+
+ Parameters
+ ----------
+ name : str, optional
+ URL-friendly identifier. Allows alphanumeric characters, hyphens ("-"), and underscores ("_").
+ title : str, optional
+ Content title. Default is None
+ description : str, optional
+ Content description.
+ access_type : Literal['all', 'acl', 'logged_in'], optional
+ How content manages viewers.
+ owner_guid : str, optional
+ The unique identifier of the user who owns this content item.
+ connection_timeout : int, optional
+ Max seconds without data exchange.
+ read_timeout : int, optional
+ Max seconds without data received.
+ init_timeout : int, optional
+ Max startup time for interactive apps.
+ idle_timeout : int, optional
+ Max idle time before process termination.
+ max_processes : int, optional
+ Max concurrent processes allowed.
+ min_processes : int, optional
+ Min concurrent processes required.
+ max_conns_per_process : int, optional
+ Max client connections per process.
+ load_factor : float, optional
+ Aggressiveness in spawning new processes (0.0 - 1.0).
+ cpu_request : float, optional
+ Min CPU units required (1 unit = 1 core).
+ cpu_limit : float, optional
+ Max CPU units allowed.
+ memory_request : int, optional
+ Min memory (bytes) required.
+ memory_limit : int, optional
+ Max memory (bytes) allowed.
+ amd_gpu_limit : int, optional
+ Number of AMD GPUs allocated.
+ nvidia_gpu_limit : int, optional
+ Number of NVIDIA GPUs allocated.
+ run_as : str, optional
+ UNIX user to execute the content.
+ run_as_current_user : bool, optional
+ Run process as the visiting user (for app content). Default is False.
+ default_image_name : str, optional
+ Default image for execution if not defined in the bundle.
+ default_r_environment_management : bool, optional
+ Manage R environment for the content.
+ default_py_environment_management : bool, optional
+ Manage Python environment for the content.
+ service_account_name : str, optional
+ Kubernetes service account name for running content.
+
+ Returns
+ -------
+ Optional[ContentItem]
+
+ Examples
+ --------
+ >>> find_by(name="example-content-name")
+ """
+ ...
+
+ @overload
+ def find_one(self, *, name: Optional[str] = ..., owner_guid: Optional[str] = ..., include: Optional[Literal["owner", "tags", "vanity_url"] | list[Literal["owner", "tags", "vanity_url"]]] = ...) -> Optional[ContentItem]:
+ """Find first content result matching the specified conditions.
+
+ Parameters
+ ----------
+ name : str, optional
+ The content name specified at creation; unique within the owner's account.
+ owner_guid : str, optional
+ The UUID of the content owner.
+ include : str or list of str, optional
+ Additional details to include in the response. Allowed values: 'owner', 'tags', 'vanity_url'.
+
+ Returns
+ -------
+ Optional[ContentItem]
+ List of matching content items.
+
+ Note
+ ----
+ Specifying both `name` and `owner_guid` returns at most one content item due to uniqueness.
+ """
+ ...
+
+ @overload
+ def find_one(self, *, name: Optional[str] = ..., owner_guid: Optional[str] = ..., include: Optional[Literal["owner", "tags"] | list[Literal["owner", "tags"]]] = ...) -> Optional[ContentItem]:
+ """Find first content result matching the specified conditions.
+
+ **Applies to Connect versions prior to 2024.06.0.**
+
+ Parameters
+ ----------
+ name : str, optional
+ The content name specified at creation; unique within the owner's account.
+ owner_guid : str, optional
+ The UUID of the content owner.
+ include : str or list of str, optional
+ Additional details to include in the response. Allowed values: 'owner', 'tags'.
+
+ Returns
+ -------
+ Optional[ContentItem]
+ List of matching content items.
+
+ Note
+ ----
+ Specifying both `name` and `owner_guid` returns at most one content item due to uniqueness.
+ """
+ ...
+
+ @overload
+ def find_one(self, **conditions) -> Optional[ContentItem]:
+ ...
+
+ def find_one(self, **conditions) -> Optional[ContentItem]:
+ """Find first content result matching the specified conditions.
+
+ Returns
+ -------
+ Optional[ContentItem]
+ """
+ ...
+
+ def get(self, guid: str) -> ContentItem:
+ """Get a content item.
+
+ Parameters
+ ----------
+ guid : str
+
+ Returns
+ -------
+ ContentItem
+ """
+ ...
+
+
+
+from typing_extensions import Protocol, TYPE_CHECKING
+from .client import Client
+
+if TYPE_CHECKING:
+ ...
+def requires(version: str): # -> Callable[..., _Wrapped[Callable[..., Any], Any, Callable[..., Any], Any]]:
+ ...
+
+class Context:
+ def __init__(self, client: Client) -> None:
+ ...
+
+ @property
+ def version(self) -> str | None:
+ ...
+
+ @version.setter
+ def version(self, value: str | None): # -> None:
+ ...
+
+
+
+class ContextManager(Protocol):
+ _ctx: Context
+ ...
+
+
+
+from dataclasses import dataclass
+from typing_extensions import Any, Generator, List, TYPE_CHECKING
+from .context import Context
+
+if TYPE_CHECKING:
+ ...
+_MAX_PAGE_SIZE = ...
+@dataclass
+class CursorPage:
+ paging: dict
+ results: List[dict]
+ ...
+
+
+class CursorPaginator:
+ def __init__(self, ctx: Context, path: str, params: dict[str, Any] | None = ...) -> None:
+ ...
+
+ def fetch_results(self) -> List[dict]:
+ """Fetch results.
+
+ Collects all results from all pages.
+
+ Returns
+ -------
+ List[dict]
+ A coalesced list of all results.
+ """
+ ...
+
+ def fetch_pages(self) -> Generator[CursorPage, None, None]:
+ """Fetch pages.
+
+ Yields
+ ------
+ Generator[Page, None, None]
+ """
+ ...
+
+ def fetch_page(self, next_page: str | None = ...) -> CursorPage:
+ """Fetch a page.
+
+ Parameters
+ ----------
+ next : str | None, optional
+ the next page identifier or None to fetch the first page, by default None
+
+ Returns
+ -------
+ Page
+ """
+ ...
+
+
+
+from typing_extensions import Any, Iterator, List, MutableMapping, Optional, TYPE_CHECKING
+from .resources import Resources
+from .context import Context
+
+"""Environment variable resources."""
+if TYPE_CHECKING:
+ ...
+class EnvVars(Resources, MutableMapping[str, Optional[str]]):
+ def __init__(self, ctx: Context, content_guid: str) -> None:
+ ...
+
+ def __delitem__(self, key: str, /) -> None:
+ """Delete the environment variable.
+
+ Parameters
+ ----------
+ key : str
+ The name of the environment variable to delete.
+
+ Examples
+ --------
+ >>> vars = EnvVars(params, content_guid)
+ >>> del vars["DATABASE_URL"]
+ """
+ ...
+
+ def __getitem__(self, key: Any) -> Any:
+ ...
+
+ def __iter__(self) -> Iterator:
+ ...
+
+ def __len__(self): # -> int:
+ ...
+
+ def __setitem__(self, key: str, value: Optional[str], /) -> None:
+ """Set environment variable.
+
+ Set the environment variable for content.
+
+ Parameters
+ ----------
+ key : str
+ The name of the environment variable to set.
+ value : str
+ The value assigned to the environment variable.
+
+ Examples
+ --------
+ >>> vars = EnvVars(params, content_guid)
+ >>> vars["DATABASE_URL"] = "postgres://user:password@localhost:5432/database"
+ """
+ ...
+
+ def clear(self) -> None:
+ """Remove all environment variables.
+
+ Examples
+ --------
+ >>> clear()
+ """
+ ...
+
+ def create(self, key: str, value: str, /) -> None:
+ """Create an environment variable.
+
+ Set an environment variable with the provided key and value. If the key already exists, its value is overwritten without warning to the provided value.
+
+ Parameters
+ ----------
+ key : str
+ The name of the environment variable to create.
+ value : str
+ The value assigned to the environment variable.
+
+ Examples
+ --------
+ >>> create(
+ ... "DATABASE_URL",
+ ... "postgres://user:password@localhost:5432/database",
+ ... )
+ """
+ ...
+
+ def delete(self, key: str, /) -> None:
+ """Delete the environment variable.
+
+ Parameters
+ ----------
+ key : str
+ The name of the environment variable to delete.
+
+ Examples
+ --------
+ >>> delete("DATABASE_URL")
+ """
+ ...
+
+ def find(self) -> List[str]:
+ """Find environment variables.
+
+ List the names of the defined environment variables.
+
+ Returns
+ -------
+ List[str]
+ Environment variable names.
+
+ Notes
+ -----
+ The Connect environment variables API does support retrieving the environment variable's value.
+
+ Examples
+ --------
+ >>> find()
+ ['DATABASE_URL']
+ """
+ ...
+
+ def items(self):
+ ...
+
+ def update(self, other=..., /, **kwargs: Optional[str]) -> None:
+ """
+ Update environment variables.
+
+ Updates environment variables with the provided key-value pairs. Accepts a dictionary, an iterable of key-value pairs, or keyword arguments to update the environment variables. All keys and values must be str types.
+
+ Parameters
+ ----------
+ other : dict, iterable of tuples, optional
+ A dictionary or an iterable of key-value pairs to update the environment variables. By default, it is None.
+ **kwargs : str
+ Additional key-value pairs to update the environment variables.
+
+ Raises
+ ------
+ TypeError
+ If the type of 'other' is not a dictionary or an iterable of key-value pairs.
+
+ Examples
+ --------
+ Update using keyword arguments:
+ >>> update(DATABASE_URL="postgres://user:password@localhost:5432/database")
+
+ Update using multiple keyword arguments:
+ >>> update(
+ ... DATABASE_URL="postgres://localhost:5432/database",
+ ... DATABASE_USERNAME="user",
+ ... DATABASE_PASSWORD="password",
+ ... )
+
+ Update using a dictionary:
+ >>> update(
+ ... {
+ ... "DATABASE_URL": "postgres://localhost:5432/database",
+ ... "DATABASE_USERNAME": "user",
+ ... "DATABASE_PASSWORD": "password",
+ ... }
+ ... )
+
+ Update using an iterable of key-value pairs:
+ >>> update(
+ ... [
+ ... ("DATABASE_URL", "postgres://localhost:5432/database"),
+ ... ("DATABASE_USERNAME", "user"),
+ ... ("DATABASE_PASSWORD", "password"),
+ ... ]
+ ... )
+ """
+ ...
+
+
+
+from abc import abstractmethod
+from typing_extensions import List, Literal, Protocol, TypedDict, runtime_checkable
+from .resources import Resource, ResourceSequence
+
+"""Environment resources."""
+MatchingType = Literal["any", "exact", "none"]
+class Installation(TypedDict):
+ """Interpreter installation in an execution environment."""
+ path: str
+ version: str
+ ...
+
+
+class Installations(TypedDict):
+ """Interpreter installations in an execution environment."""
+ installations: List[Installation]
+ ...
+
+
+class Environment(Resource):
+ @abstractmethod
+ def destroy(self) -> None:
+ """Destroy the environment.
+
+ Warnings
+ --------
+ This operation is irreversible.
+
+ Note
+ ----
+ This action requires administrator privileges.
+ """
+ ...
+
+ @abstractmethod
+ def update(self, *, title: str, description: str | None = ..., matching: MatchingType | None = ..., supervisor: str | None = ..., python: Installations | None = ..., quarto: Installations | None = ..., r: Installations | None = ..., tensorflow: Installations | None = ...) -> None:
+ """Update the environment.
+
+ Parameters
+ ----------
+ title : str
+ A human-readable title.
+ description : str | None, optional, not required
+ A human-readable description.
+ matching : MatchingType, optional, not required
+ Directions for how the environment is considered for selection
+ supervisor : str | None, optional, not required
+ Path to the supervisor script.
+ python : Installations, optional, not required
+ The Python installations available in this environment
+ quarto : Installations, optional, not required
+ The Quarto installations available in this environment
+ r : Installations, optional, not required
+ The R installations available in this environment
+ tensorflow : Installations, optional, not required
+ The Tensorflow installations available in this environment
+
+ Note
+ ----
+ This action requires administrator privileges.
+ """
+ ...
+
+
+
+@runtime_checkable
+class Environments(ResourceSequence[Environment], Protocol):
+ def create(self, *, title: str, name: str, cluster_name: str | Literal["Kubernetes"], matching: MatchingType = ..., description: str | None = ..., supervisor: str | None = ..., python: Installations | None = ..., quarto: Installations | None = ..., r: Installations | None = ..., tensorflow: Installations | None = ...) -> Environment:
+ """Create an environment.
+
+ Parameters
+ ----------
+ title : str
+ A human-readable title.
+ name : str
+ The container image name used for execution in this environment.
+ cluster_name : str | Literal["Kubernetes"]
+ The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
+ description : str, optional
+ A human-readable description.
+ matching : MatchingType
+ Directions for how the environment is considered for selection, by default is "any".
+ supervisor : str, optional
+ Path to the supervisor script
+ python : Installations, optional
+ The Python installations available in this environment
+ quarto : Installations, optional
+ The Quarto installations available in this environment
+ r : Installations, optional
+ The R installations available in this environment
+ tensorflow : Installations, optional
+ The Tensorflow installations available in this environment
+
+ Returns
+ -------
+ Environment
+
+ Note
+ ----
+ This action requires administrator privileges.
+ """
+ ...
+
+ def find(self, guid: str, /) -> Environment:
+ ...
+
+ def find_by(self, *, id: str = ..., guid: str = ..., created_time: str = ..., updated_time: str = ..., title: str = ..., name: str = ..., description: str | None = ..., cluster_name: str | Literal["Kubernetes"] = ..., environment_type: str | Literal["Kubernetes"] = ..., matching: MatchingType = ..., supervisor: str | None = ..., python: Installations | None = ..., quarto: Installations | None = ..., r: Installations | None = ..., tensorflow: Installations | None = ...) -> Environment | None:
+ """Find the first record matching the specified conditions.
+
+ There is no implied ordering, so if order matters, you should specify it yourself.
+
+ Parameters
+ ----------
+ id : str
+ The numerical identifier.
+ guid : str
+ The unique identifier.
+ created_time : str
+ The timestamp (RFC3339) when the environment was created.
+ updated_time : str
+ The timestamp (RFC3339) when the environment was updated.
+ title : str
+ A human-readable title.
+ name : str
+ The container image name used for execution in this environment.
+ description : str, optional
+ A human-readable description.
+ cluster_name : str | Literal["Kubernetes"]
+ The cluster identifier for this environment. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
+ environment_type : str | Literal["Kubernetes"]
+ The cluster environment type. Defaults to "Kubernetes" when Off-Host-Execution is enabled.
+ matching : MatchingType
+ Directions for how the environment is considered for selection.
+ supervisor : str, optional
+ Path to the supervisor script
+ python : Installations, optional
+ The Python installations available in this environment
+ quarto : Installations, optional
+ The Quarto installations available in this environment
+ r : Installations, optional
+ The R installations available in this environment
+ tensorflow : Installations, optional
+ The Tensorflow installations available in this environment
+
+ Returns
+ -------
+ Environment | None
+
+ Note
+ ----
+ This action requires administrator or publisher privileges.
+ """
+ ...
+
+
+
+from typing_extensions import Any
+
+class ClientError(Exception):
+ def __init__(self, error_code: int, error_message: str, http_status: int, http_message: str, payload: Any = ...) -> None:
+ ...
+
+
+
+from typing_extensions import List, Optional, TYPE_CHECKING, overload
+from .resources import BaseResource, Resources
+from .context import Context
+from .users import User
+
+"""Group resources."""
+if TYPE_CHECKING:
+ ...
+class Group(BaseResource):
+ def __init__(self, ctx: Context, **kwargs) -> None:
+ ...
+
+ @property
+ def members(self) -> GroupMembers:
+ """Get the group members.
+
+ Returns
+ -------
+ GroupMembers
+ All the users in the group.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+ group_users = group.members.find()
+
+ # Get count of group members
+ group_user_count = group.members.count()
+ ```
+
+ """
+ ...
+
+ def delete(self) -> None:
+ """Delete the group.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+
+ # Delete the group
+ group.delete()
+ ```
+ """
+ ...
+
+
+
+class GroupMembers(Resources):
+ def __init__(self, ctx: Context, group_guid: str) -> None:
+ ...
+
+ @overload
+ def add(self, user: User, /) -> None:
+ ...
+
+ @overload
+ def add(self, /, *, user_guid: str) -> None:
+ ...
+
+ def add(self, user: Optional[User] = ..., /, *, user_guid: Optional[str] = ...) -> None:
+ """Add a user to the group.
+
+ Parameters
+ ----------
+ user : User
+ User object to add to the group. Only one of `user=` or `user_guid=` can be provided.
+ user_guid : str
+ The user GUID.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+ user = client.users.get("USER_GUID_HERE")
+
+ # Add a user to the group
+ group.members.add(user)
+
+ # Add multiple users to the group
+ users = client.users.find()
+ for user in users:
+ group.members.add(user)
+
+ # Add a user to the group by GUID
+ group.members.add(user_guid="USER_GUID_HERE")
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/groups/-group_guid-/members
+ """
+ ...
+
+ @overload
+ def delete(self, user: User, /) -> None:
+ ...
+
+ @overload
+ def delete(self, /, *, user_guid: str) -> None:
+ ...
+
+ def delete(self, user: Optional[User] = ..., /, *, user_guid: Optional[str] = ...) -> None:
+ """Remove a user from the group.
+
+ Parameters
+ ----------
+ user : User
+ User object to add to the group. Only one of `user=` or `user_guid=` can be provided.
+ user_guid : str
+ The user GUID.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+
+ # Remove a user from the group
+ first_user = group.members.find()[0]
+ group.members.delete(first_user)
+
+ # Remove multiple users from the group
+ group_users = group.members.find()[:2]
+ for group_user in group_users:
+ group.members.delete(group_user)
+
+ # Remove a user from the group by GUID
+ group.members.delete(user_guid="USER_GUID_HERE")
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#delete-/v1/groups/-group_guid-/members/-user_guid-
+ """
+ ...
+
+ def find(self) -> list[User]:
+ """Find group members.
+
+ Returns
+ -------
+ list[User]
+ All the users in the group.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+
+ # Find all users in the group
+ group_users = group.members.find()
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+ """
+ ...
+
+ def count(self) -> int:
+ """Count the number of group members.
+
+ Returns
+ -------
+ int
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+
+ # Get count of group members
+ group_user_count = group.members.count()
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+ """
+ ...
+
+
+
+class Groups(Resources):
+ """Groups resource."""
+ @overload
+ def create(self, *, name: str, unique_id: str | None) -> Group:
+ """Create a group.
+
+ Parameters
+ ----------
+ name: str
+ unique_id: str | None
+
+ Returns
+ -------
+ Group
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/groups
+ """
+ ...
+
+ @overload
+ def create(self, **kwargs) -> Group:
+ """Create a group.
+
+ Returns
+ -------
+ Group
+ """
+ ...
+
+ def create(self, **kwargs) -> Group:
+ """Create a group.
+
+ Parameters
+ ----------
+ name: str
+ unique_id: str | None
+
+ Returns
+ -------
+ Group
+ """
+ ...
+
+ @overload
+ def find(self, *, prefix: str = ...) -> List[Group]:
+ ...
+
+ @overload
+ def find(self, **kwargs) -> List[Group]:
+ ...
+
+ def find(self, **kwargs): # -> list[Group]:
+ """Find groups.
+
+ Parameters
+ ----------
+ prefix: str
+ Filter by group name prefix. Casing is ignored.
+
+ Returns
+ -------
+ List[Group]
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups
+ """
+ ...
+
+ @overload
+ def find_one(self, *, prefix: str = ...) -> Group | None:
+ ...
+
+ @overload
+ def find_one(self, **kwargs) -> Group | None:
+ ...
+
+ def find_one(self, **kwargs) -> Group | None:
+ """Find one group.
+
+ Parameters
+ ----------
+ prefix: str
+ Filter by group name prefix. Casing is ignored.
+
+ Returns
+ -------
+ Group | None
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups
+ """
+ ...
+
+ def get(self, guid: str) -> Group:
+ """Get group.
+
+ Parameters
+ ----------
+ guid : str
+
+ Returns
+ -------
+ Group
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups
+ """
+ ...
+
+ def count(self) -> int:
+ """Count the number of groups.
+
+ Returns
+ -------
+ int
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups
+ """
+ ...
+
+
+
+from requests import Response
+
+def handle_errors(response: Response, *request_hook_args, **request_hook_kwargs) -> Response:
+ ...
+
+def check_for_deprecation_header(response: Response, *args, **kwargs) -> Response:
+ """
+ Check for deprecation warnings from the server.
+
+ You might get these if you've upgraded the Connect server but not posit-sdk.
+ posit-sdk will make the right request based on the version of the server,
+ but if you have an old version of the package, it won't know the new URL
+ to request.
+ """
+ ...
+
+
+
+from typing_extensions import Iterable, Literal, Protocol, runtime_checkable
+from .resources import Resource, ResourceSequence
+
+"""Job resources."""
+JobTag = Literal["unknown", "build_report", "build_site", "build_jupyter", "packrat_restore", "python_restore", "configure_report", "run_app", "run_api", "run_tensorflow", "run_python_api", "run_dash_app", "run_streamlit", "run_bokeh_app", "run_fastapi_app", "run_pyshiny_app", "render_shiny", "run_voila_app", "testing", "git", "val_py_ext_pkg", "val_r_ext_pkg", "val_r_install",]
+StatusCode = Literal[0, 1, 2]
+class Job(Resource, Protocol):
+ def destroy(self) -> None:
+ """Destroy the job.
+
+ Warnings
+ --------
+ This operation is irreversible.
+
+ Note
+ ----
+ This action requires administrator, owner, or collaborator privileges.
+ """
+ ...
+
+
+
+@runtime_checkable
+class Jobs(ResourceSequence[Job], Protocol):
+ def fetch(self) -> Iterable[Job]:
+ """Fetch all jobs.
+
+ Fetches all jobs from Connect.
+
+ Returns
+ -------
+ List[Job]
+ """
+ ...
+
+ def find(self, key: str, /) -> Job:
+ """
+ Find a Job by its key.
+
+ Fetches the Job from Connect by it's key.
+
+ Parameters
+ ----------
+ key : str
+ The unique identifier of the Job.
+
+ Returns
+ -------
+ Jobs
+ """
+ ...
+
+ def find_by(self, *, id: str = ..., ppid: str | None = ..., pid: str = ..., key: str = ..., remote_id: str | None = ..., app_id: str = ..., variant_id: str = ..., bundle_id: str = ..., start_time: str = ..., end_time: str | None = ..., last_heartbeat_time: str = ..., queued_time: str | None = ..., status: StatusCode = ..., exit_code: int | None = ..., hostname: str = ..., cluster: str | None = ..., image: str | None = ..., run_as: str = ..., queue_name: str | None = ..., tag: JobTag = ...) -> Job | None:
+ """Find the first record matching the specified conditions.
+
+ There is no implied ordering, so if order matters, you should specify it yourself.
+
+ id : str, not required
+ A unique identifier for the job.
+ ppid : Optional[str], not required
+ Identifier of the parent process.
+ pid : str, not required
+ Identifier of the process running the job.
+ key : str, not required
+ A unique key to identify this job.
+ remote_id : Optional[str], not required
+ Identifier for off-host execution configurations.
+ app_id : str, not required
+ Identifier of the parent content associated with the job.
+ variant_id : str, not required
+ Identifier of the variant responsible for the job.
+ bundle_id : str, not required
+ Identifier of the content bundle linked to the job.
+ start_time : str, not required
+ RFC3339 timestamp indicating when the job started.
+ end_time : Optional[str], not required
+ RFC3339 timestamp indicating when the job finished.
+ last_heartbeat_time : str, not required
+ RFC3339 timestamp of the last recorded activity for the job.
+ queued_time : Optional[str], not required
+ RFC3339 timestamp when the job was added to the queue.
+ status : int, not required
+ Current status. Options are 0 (Active), 1 (Finished), and 2 (Finalized)
+ exit_code : Optional[int], not required
+ The job's exit code, available after completion.
+ hostname : str, not required
+ Name of the node processing the job.
+ cluster : Optional[str], not required
+ Location where the job runs, either 'Local' or the cluster name.
+ image : Optional[str], not required
+ Location of the content in clustered environments.
+ run_as : str, not required
+ UNIX user responsible for executing the job.
+ queue_name : Optional[str], not required
+ Name of the queue processing the job, relevant for scheduled reports.
+ tag : JobTag, not required
+ A tag categorizing the job type. Options are build_jupyter, build_report, build_site, configure_report, git, packrat_restore, python_restore, render_shiny, run_api, run_app, run_bokeh_app, run_dash_app, run_fastapi_app, run_pyshiny_app, run_python_api, run_streamlit, run_tensorflow, run_voila_app, testing, unknown, val_py_ext_pkg, val_r_ext_pkg, and val_r_install.
+
+ Returns
+ -------
+ Job | None
+
+ Note
+ ----
+ This action requires administrator, owner, or collaborator privileges.
+ """
+ ...
+
+
+
+from .context import Context
+from .users import User
+
+def get(ctx: Context) -> User:
+ """
+ Gets the current user.
+
+ Args:
+ config (Config): The configuration object containing the URL.
+ session (requests.Session): The session object used for making HTTP requests.
+
+ Returns
+ -------
+ User: The current user.
+ """
+ ...
+
+
+
+from typing_extensions import Iterable, Literal, Protocol
+from .resources import Resource, ResourceSequence
+
+"""Package resources."""
+class ContentPackage(Resource, Protocol):
+ ...
+
+
+class ContentPackages(ResourceSequence[ContentPackage], Protocol):
+ def fetch(self, *, language: Literal["python", "r"] = ..., name: str = ..., version: str = ..., hash: str | None = ...) -> Iterable[ContentPackage]:
+ """
+ Fetch all records matching the specified conditions.
+
+ Parameters
+ ----------
+ language : {"python", "r"}, not required
+ Programming language ecosystem, options are 'python' and 'r'
+ name : str, not required
+ The package name
+ version : str, not required
+ The package version
+ hash : str or None, optional, not required
+ Package description hash for R packages.
+
+ Returns
+ -------
+ List[ContentPackage]
+ The first record matching the specified conditions, or `None` if no such record exists.
+ """
+ ...
+
+ def find_by(self, *, language: Literal["python", "r"] = ..., name: str = ..., version: str = ..., hash: str | None = ...) -> ContentPackage | None:
+ """
+ Find the first record matching the specified conditions.
+
+ There is no implied ordering, so if order matters, you should specify it yourself.
+
+ Parameters
+ ----------
+ language : {"python", "r"}, not required
+ Programming language ecosystem, options are 'python' and 'r'
+ name : str, not required
+ The package name
+ version : str, not required
+ The package version
+ hash : str or None, optional, not required
+ Package description hash for R packages.
+
+ Returns
+ -------
+ ContentPackage | None
+ The first record matching the specified conditions, or `None` if no such record exists.
+ """
+ ...
+
+
+
+class Package(Resource, Protocol):
+ ...
+
+
+class Packages(ResourceSequence[Package], Protocol):
+ def fetch(self, *, language: Literal["python", "r"] = ..., name: str = ..., version: str = ..., hash: str | None = ..., bundle_id: str = ..., app_id: str = ..., app_guid: str = ...) -> Iterable[Package]:
+ """
+ Fetch all records matching the specified conditions.
+
+ Parameters
+ ----------
+ language : {"python", "r"}, not required
+ Programming language ecosystem, options are 'python' and 'r'
+ name : str, not required
+ The package name
+ version : str, not required
+ The package version
+ hash : str or None, optional, not required
+ Package description hash for R packages.
+ bundle_id: str, not required
+ The unique identifier of the bundle this package is associated with.
+ app_id: str, not required
+ The numerical identifier of the application this package is associated with.
+ app_guid: str, not required
+ The unique identifier of the application this package is associated with.
+
+ Returns
+ -------
+ List[Package]
+ The first record matching the specified conditions, or `None` if no such record exists.
+ """
+ ...
+
+ def find_by(self, *, language: Literal["python", "r"] = ..., name: str = ..., version: str = ..., hash: str | None = ..., bundle_id: str = ..., app_id: str = ..., app_guid: str = ...) -> Package | None:
+ """
+ Find the first record matching the specified conditions.
+
+ There is no implied ordering, so if order matters, you should specify it yourself.
+
+ Parameters
+ ----------
+ language : {"python", "r"}, not required
+ Programming language ecosystem, options are 'python' and 'r'
+ name : str, not required
+ The package name
+ version : str, not required
+ The package version
+ hash : str or None, optional, not required
+ Package description hash for R packages.
+ bundle_id: str, not required
+ The unique identifier of the bundle this package is associated with.
+ app_id: str, not required
+ The numerical identifier of the application this package is associated with.
+ app_guid: str, not required
+ The unique identifier of the application this package is associated with.
+
+ Returns
+ -------
+ Package | None
+ The first record matching the specified conditions, or `None` if no such record exists.
+ """
+ ...
+
+
+
+from dataclasses import dataclass
+from typing_extensions import Generator, List, TYPE_CHECKING
+from .context import Context
+
+if TYPE_CHECKING:
+ ...
+_MAX_PAGE_SIZE = ...
+@dataclass
+class Page:
+ """
+ Represents a page of results returned by the paginator.
+
+ Attributes
+ ----------
+ current_page (int): The current page number.
+ total (int): The total number of results.
+ results (List[dict]): The list of results on the current page.
+ """
+ current_page: int
+ total: int
+ results: List[dict]
+ ...
+
+
+class Paginator:
+ """
+ A class for paginating through API results.
+
+ Args:
+ session (requests.Session): The session object to use for making API requests.
+ url (str): The URL of the paginated API endpoint.
+
+ Attributes
+ ----------
+ session (requests.Session): The session object to use for making API requests.
+ url (str): The URL of the paginated API endpoint.
+ """
+ def __init__(self, ctx: Context, path: str, params: dict | None = ...) -> None:
+ ...
+
+ def fetch_results(self) -> List[dict]:
+ """
+ Fetches and returns all the results from the paginated API endpoint.
+
+ Returns
+ -------
+ A list of dictionaries representing the fetched results.
+ """
+ ...
+
+ def fetch_pages(self) -> Generator[Page, None, None]:
+ """
+ Fetches pages of results from the API.
+
+ Yields
+ ------
+ Page: A page of results from the API.
+ """
+ ...
+
+ def fetch_page(self, page_number: int) -> Page:
+ """
+ Fetches a specific page of data from the API.
+
+ Args:
+ page_number (int): The page number to fetch.
+
+ Returns
+ -------
+ Page: The fetched page object.
+
+ """
+ ...
+
+
+
+from typing_extensions import List, Optional, TYPE_CHECKING, overload
+from .resources import BaseResource, Resources
+from .context import Context
+from .groups import Group
+from .users import User
+
+"""Permission resources."""
+if TYPE_CHECKING:
+ ...
+class Permission(BaseResource):
+ def destroy(self) -> None:
+ """Destroy the permission."""
+ ...
+
+ @overload
+ def update(self, *args, role: str, **kwargs) -> None:
+ """Update the permission.
+
+ Parameters
+ ----------
+ role : str
+ The principal role.
+ """
+ ...
+
+ @overload
+ def update(self, *args, **kwargs) -> None:
+ """Update the permission."""
+ ...
+
+ def update(self, *args, **kwargs) -> None:
+ """Update the permission."""
+ ...
+
+
+
+class Permissions(Resources):
+ def __init__(self, ctx: Context, content_guid: str) -> None:
+ ...
+
+ def count(self) -> int:
+ """Count the number of permissions.
+
+ Returns
+ -------
+ int
+ """
+ ...
+
+ @overload
+ def create(self, /, *, principal_guid: str, principal_type: str, role: str) -> Permission:
+ ...
+
+ @overload
+ def create(self, principal: User | Group, /, *, role: str) -> Permission:
+ ...
+
+ def create(self, principal: Optional[User | Group] = ..., /, **kwargs) -> Permission:
+ """Create a permission.
+
+ Parameters
+ ----------
+ principal : User | Group
+ The principal user or group to add.
+ role : str
+ The principal role. Currently only `"viewer"` and `"owner"` are supported.
+ principal_guid : str
+ User guid or Group guid.
+ principal_type : str
+ The principal type. Either `"user"` or `"group"`.
+ role : str
+ The principal role. Currently only `"viewer"` and `"owner"` are supported
+
+ Returns
+ -------
+ Permission
+ The created permission.
+
+ Examples
+ --------
+ ```python
+ from posit import connect
+
+ client = connect.Client()
+ content_item = client.content.get(content_guid)
+
+ # New permission role
+ role = "viewer" # Or "owner"
+
+ # Example groups and users
+ groups = client.groups.find(prefix="GROUP_NAME_PREFIX_HERE")
+ group = groups[0]
+ user = client.users.get("USER_GUID_HERE")
+ users_and_groups = [user, *groups]
+
+ # Add a group permission
+ content_item.permissions.create(group, role=role)
+ # Add a user permission
+ content_item.permissions.create(user, role=role)
+
+ # Add many group and user permissions with the same role
+ for principal in users_and_groups:
+ content_item.permissions.create(principal, role=role)
+
+ # Add a group permission manually
+ content_item.permissions.create(
+ principal_guid=group["guid"],
+ principal_type="group",
+ role=role,
+ )
+ # Add a user permission manually
+ content_item.permissions.create(
+ principal_guid=user["guid"],
+ principal_type="user",
+ role=role,
+ )
+
+ # Confirm new permissions
+ content_item.permissions.find()
+ ```
+ """
+ ...
+
+ def find(self, **kwargs) -> List[Permission]:
+ """Find permissions.
+
+ Returns
+ -------
+ List[Permission]
+ """
+ ...
+
+ def find_one(self, **kwargs) -> Permission | None:
+ """Find a permission.
+
+ Returns
+ -------
+ Permission | None
+ """
+ ...
+
+ def get(self, uid: str) -> Permission:
+ """Get a permission.
+
+ Parameters
+ ----------
+ uid : str
+ The permission id.
+
+ Returns
+ -------
+ Permission
+ """
+ ...
+
+ def destroy(self, permission: str | Group | User | Permission, /) -> None:
+ """Remove supplied content item permission.
+
+ Removes provided permission from the content item's permissions.
+
+ Parameters
+ ----------
+ permission : str | Group | User | Permission
+ The content item permission to remove. If a `str` is received, it is compared against
+ the `Permissions`'s `principal_guid`. If a `Group` or `User` is received, the associated
+ `Permission` will be removed.
+
+ Examples
+ --------
+ ```python
+ from posit import connect
+
+ #### User-defined inputs ####
+ # 1. specify the guid for the content item
+ content_guid = "CONTENT_GUID_HERE"
+ # 2. specify either the principal_guid or group name prefix
+ principal_guid = "USER_OR_GROUP_GUID_HERE"
+ group_name_prefix = "GROUP_NAME_PREFIX_HERE"
+ ############################
+
+ client = connect.Client()
+ content_item = client.content.get(content_guid)
+
+ # Remove a single permission by principal_guid
+ content_item.permissions.destroy(principal_guid)
+
+ # Remove by user (if principal_guid is a user)
+ user = client.users.get(principal_guid)
+ content_item.permissions.destroy(user)
+
+ # Remove by group (if principal_guid is a group)
+ group = client.groups.get(principal_guid)
+ content_item.permissions.destroy(group)
+
+ # Remove all groups with a matching prefix name
+ groups = client.groups.find(prefix=group_name_prefix)
+ for group in groups:
+ content_item.permissions.destroy(group)
+
+ # Confirm new permissions
+ content_item.permissions.find()
+ ```
+ """
+ ...
+
+
+
+from typing_extensions import Optional, Protocol, overload, runtime_checkable
+from .resources import Resource, _Resource
+
+"""Repository resources."""
+class _ContentItemRepository(_Resource):
+ def update(self, **attributes) -> None:
+ ...
+
+
+
+@runtime_checkable
+class ContentItemRepository(Resource, Protocol):
+ """
+ Content items GitHub repository information.
+
+ See Also
+ --------
+ * Get info: https://docs.posit.co/connect/api/#get-/v1/content/-guid-/repository
+ * Delete info: https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository
+ * Update info: https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository
+ """
+ def destroy(self) -> None:
+ """
+ Delete the content's git repository location.
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#delete-/v1/content/-guid-/repository
+ """
+ ...
+
+ def update(self, *, repository: Optional[str] = ..., branch: str = ..., directory: str = ..., polling: bool = ...) -> None:
+ """Update the content's repository.
+
+ Parameters
+ ----------
+ repository: str, optional
+ URL for the repository. Default is None.
+ branch: str, optional
+ The tracked Git branch. Default is 'main'.
+ directory: str, optional
+ Directory containing the content. Default is '.'
+ polling: bool, optional
+ Indicates that the Git repository is regularly polled. Default is False.
+
+ Returns
+ -------
+ None
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#patch-/v1/content/-guid-/repository
+ """
+ ...
+
+
+
+class ContentItemRepositoryMixin:
+ @property
+ def repository(self: Resource) -> ContentItemRepository | None:
+ ...
+
+ @overload
+ def create_repository(self: Resource, /, *, repository: Optional[str] = ..., branch: str = ..., directory: str = ..., polling: bool = ...) -> ContentItemRepository:
+ ...
+
+ @overload
+ def create_repository(self: Resource, /, **attributes) -> ContentItemRepository:
+ ...
+
+ def create_repository(self: Resource, /, **attributes) -> ContentItemRepository:
+ """Create repository.
+
+ Parameters
+ ----------
+ repository : str
+ URL for the respository.
+ branch : str, optional
+ The tracked Git branch. Default is 'main'.
+ directory : str, optional
+ Directory containing the content. Default is '.'.
+ polling : bool, optional
+ Indicates that the Git repository is regularly polled. Default is False.
+
+ Returns
+ -------
+ ContentItemRepository
+ """
+ ...
+
+
+
+from abc import ABC
+from typing_extensions import Any, Hashable, Iterable, Iterator, List, Protocol, Sequence, SupportsIndex, TYPE_CHECKING, TypeVar, overload
+from .context import Context
+
+if TYPE_CHECKING:
+ ...
+class BaseResource(dict):
+ def __init__(self, ctx: Context, /, **kwargs) -> None:
+ ...
+
+ def __getattr__(self, name):
+ ...
+
+ def update(self, *args, **kwargs): # -> None:
+ ...
+
+
+
+class Resources:
+ def __init__(self, ctx: Context) -> None:
+ ...
+
+
+
+class Active(ABC, BaseResource):
+ def __init__(self, ctx: Context, path: str, /, **attributes) -> None:
+ """A dict abstraction for any HTTP endpoint that returns a singular resource.
+
+ Extends the `Resource` class and provides additional functionality for via the session context and an optional parent resource.
+
+ Parameters
+ ----------
+ ctx : Context
+ The context object containing the session and URL for API interactions.
+ path : str
+ The HTTP path component for the resource endpoint
+ **attributes : dict
+ Resource attributes passed
+ """
+ ...
+
+
+
+class Resource(Protocol):
+ _ctx: Context
+ _path: str
+ def __getitem__(self, key: Hashable, /) -> Any:
+ ...
+
+
+
+class _Resource(dict, Resource):
+ def __init__(self, ctx: Context, path: str, **attributes) -> None:
+ ...
+
+ def destroy(self) -> None:
+ ...
+
+ def update(self, **attributes): # -> None:
+ ...
+
+
+
+T = TypeVar("T", bound=Resource)
+class ResourceSequence(Protocol[T]):
+ @overload
+ def __getitem__(self, index: SupportsIndex, /) -> T:
+ ...
+
+ @overload
+ def __getitem__(self, index: slice, /) -> List[T]:
+ ...
+
+ def __len__(self) -> int:
+ ...
+
+ def __iter__(self) -> Iterator[T]:
+ ...
+
+ def __str__(self) -> str:
+ ...
+
+ def __repr__(self) -> str:
+ ...
+
+
+
+class _ResourceSequence(Sequence[T], ResourceSequence[T]):
+ def __init__(self, ctx: Context, path: str, *, uid: str = ...) -> None:
+ ...
+
+ def __getitem__(self, index): # -> Any:
+ ...
+
+ def __len__(self) -> int:
+ ...
+
+ def __iter__(self) -> Iterator[T]:
+ ...
+
+ def __str__(self) -> str:
+ ...
+
+ def __repr__(self) -> str:
+ ...
+
+ def create(self, **attributes: Any) -> Any:
+ ...
+
+ def fetch(self, **conditions) -> Iterable[Any]:
+ ...
+
+ def find(self, *args: str) -> Any:
+ ...
+
+ def find_by(self, **conditions) -> Any | None:
+ """
+ Find the first record matching the specified conditions.
+
+ There is no implied ordering, so if order matters, you should specify it yourself.
+
+ Parameters
+ ----------
+ **conditions : Any
+
+ Returns
+ -------
+ Optional[T]
+ The first record matching the conditions, or `None` if no match is found.
+ """
+ ...
+
+
+
+class _PaginatedResourceSequence(_ResourceSequence):
+ def fetch(self, **conditions): # -> Generator[Any, Any, None]:
+ ...
+
+
+
+from typing_extensions import List, Literal, TYPE_CHECKING, TypedDict, Unpack, overload
+from .context import Context, ContextManager
+from .resources import Active
+from .tasks import Task
+
+"""System resources."""
+if TYPE_CHECKING:
+ ...
+class System(ContextManager):
+ """System information."""
+ def __init__(self, ctx: Context, path: str) -> None:
+ ...
+
+ @property
+ def caches(self) -> SystemCaches:
+ """
+ List all system caches.
+
+ Returns
+ -------
+ SystemCaches
+ Helper class for system caches.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client()
+
+ caches = client.system.caches.runtime.find()
+ ```
+
+ """
+ ...
+
+
+
+class SystemCaches(ContextManager):
+ """System caches."""
+ def __init__(self, ctx: Context, path: str) -> None:
+ ...
+
+ @property
+ def runtime(self) -> SystemRuntimeCaches:
+ """
+ System runtime caches.
+
+ Returns
+ -------
+ SystemRuntimeCaches
+ Helper class to manage system runtime caches.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client()
+
+ caches = client.system.caches.runtime.find()
+ ```
+ """
+ ...
+
+
+
+class SystemRuntimeCache(Active):
+ class _Attrs(TypedDict, total=False):
+ language: str
+ version: str
+ image_name: str
+ ...
+
+
+ def __init__(self, ctx: Context, path: str, /, **attributes: Unpack[_Attrs]) -> None:
+ ...
+
+ class _DestroyAttrs(TypedDict, total=False):
+ dry_run: bool
+ ...
+
+
+ @overload
+ def destroy(self, *, dry_run: Literal[True]) -> None:
+ ...
+
+ @overload
+ def destroy(self, *, dry_run: Literal[False] = ...) -> Task:
+ ...
+
+ def destroy(self, **kwargs) -> Task | None:
+ """
+ Remove a content runtime package cache.
+
+ This action is only available to administrators.
+
+ Parameters
+ ----------
+ dry_run : bool, optional
+ If `True`, the cache will not be destroyed, only the operation will be simulated.
+
+ Returns
+ -------
+ Task | None
+ The task object if the operation was successful. If `dry_run=True`, `None` is returned.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client()
+
+ runtime_caches = client.system.caches.runtime.find()
+ first_runtime_cache = runtime_caches[0]
+
+ # Remove the cache
+ task = first_runtime_cache.destroy(dry_run=False)
+
+ # Wait for the task to finish
+ task.wait_for()
+ ```
+ """
+ ...
+
+
+
+class SystemRuntimeCaches(ContextManager):
+ """
+ System runtime caches.
+
+ List all content runtime caches. These include packrat and Python
+ environment caches.
+
+ This information is available only to administrators.
+ """
+ def __init__(self, ctx: Context, path: str) -> None:
+ ...
+
+ def find(self) -> List[SystemRuntimeCache]:
+ """
+ List all content runtime caches.
+
+ List all content runtime caches. These include packrat and Python
+ environment caches.
+
+ This information is available only to administrators.
+
+ Returns
+ -------
+ List[SystemRuntimeCache]
+ List of all content runtime caches.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client()
+
+ runtime_caches = client.system.caches.runtime.find()
+ ```
+ """
+ ...
+
+ @overload
+ def destroy(self, /, *, language: str, version: str, image_name: str, dry_run: Literal[False] = ...) -> Task:
+ ...
+
+ @overload
+ def destroy(self, /, *, language: str, version: str, image_name: str, dry_run: Literal[True] = ...) -> None:
+ ...
+
+ def destroy(self, /, **kwargs) -> Task | None:
+ """
+ Delete a content runtime package cache.
+
+ Delete a content runtime package cache by specifying language, version, and execution
+ environment.
+
+ This action is only available to administrators.
+
+ Parameters
+ ----------
+ language : str
+ The runtime language of the cache.
+ version : str
+ The language version of the cache.
+ image_name : str
+ The name of the cache's execution environment.
+ dry_run : bool, optional
+ If `True`, the cache will not be destroyed, only the operation will be simulated.
+
+ Returns
+ -------
+ Task | None
+ The task object if the operation was successful. If `dry_run=True`, `None` is returned.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client()
+
+ runtime_caches = client.system.caches.runtime.find()
+ first_runtime_cache = runtime_caches[0]
+
+ # Remove the cache
+ task = first_runtime_cache.destroy(dry_run=False)
+
+ # Or, remove the cache by specifying the cache's attributes
+ task = client.system.caches.runtime.destroy(
+ language="Python",
+ version="3.12.5",
+ image_name="Local",
+ dry_run=False,
+ )
+ ```
+ """
+ ...
+
+
+
+from abc import ABC, abstractmethod
+from typing_extensions import NotRequired, Optional, TYPE_CHECKING, TypedDict, Unpack, overload
+from .context import Context, ContextManager
+from .resources import Active
+from .content import ContentItem
+
+"""Tag resources."""
+if TYPE_CHECKING:
+ ...
+class _RelatedTagsBase(ContextManager, ABC):
+ @abstractmethod
+ def find(self) -> list[Tag]:
+ ...
+
+
+
+class Tag(Active):
+ """Tag resource."""
+ class _Attrs(TypedDict, total=False):
+ id: str
+ name: str
+ parent_id: NotRequired[Optional[str]]
+ created_time: str
+ updated_time: str
+ ...
+
+
+ def __init__(self, ctx: Context, path: str, /, **kwargs: Unpack[Tag._Attrs]) -> None:
+ ...
+
+ @property
+ def parent_tag(self) -> Tag | None:
+ ...
+
+ @property
+ def child_tags(self) -> ChildTags:
+ """
+ Find all child tags that are direct children of this tag.
+
+ Returns
+ -------
+ ChildrenTags
+ Helper class that can `.find()` the child tags.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ mytag = client.tags.find(id="TAG_ID_HERE")
+
+ children = mytag.child_tags.find()
+ ```
+ """
+ ...
+
+ @property
+ def descendant_tags(self) -> DescendantTags:
+ """
+ Find all tags that descend from this tag.
+
+ Returns
+ -------
+ DescendantTags
+ Helper class that can `.find()` all descendant tags.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ mytag = client.tags.find(id="TAG_ID_HERE")
+
+ descendant_tags = mytag.descendant_tags.find()
+ ```
+ """
+ ...
+
+ @property
+ def content_items(self) -> TagContentItems:
+ """
+ Find all content items that are tagged with this tag.
+
+ Returns
+ -------
+ TagContentItems
+ Helper class that can `.find()` all content items.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ first_tag = client.tags.find()[0]
+
+ first_tag_content_items = first_tag.content_items.find()
+ ```
+ """
+ ...
+
+ def destroy(self) -> None:
+ """
+ Removes the tag.
+
+ Deletes a tag, including all descendants in its own tag hierarchy.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ first_tag = client.tags.find()[0]
+
+ # Remove the tag
+ first_tag.destroy()
+ ```
+ """
+ ...
+
+ @overload
+ def update(self, /, *, name: str = ..., parent: Tag | None = ...) -> None:
+ ...
+
+ @overload
+ def update(self, /, *, name: str = ..., parent_id: str | None = ...) -> None:
+ ...
+
+ def update(self, **kwargs) -> None:
+ """
+ Update the tag.
+
+ Parameters
+ ----------
+ name : str
+ The name of the tag.
+ parent : Tag | None, optional
+ The parent `Tag` object. If there is no parent, the tag is a top-level tag. To remove
+ the parent tag, set the value to `None`. Only one of `parent` or `parent_id` can be
+ provided.
+ parent_id : str | None, optional
+ The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
+ To remove the parent tag, set the value to `None`.
+
+ Returns
+ -------
+ Tag
+ Updated tag object.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ last_tag = client.tags.find()[-1]
+
+ # Update the tag's name
+ updated_tag = last_tag.update(name="new_name")
+
+ # Remove the tag's parent
+ updated_tag = last_tag.update(parent=None)
+ updated_tag = last_tag.update(parent_id=None)
+
+ # Update the tag's parent
+ parent_tag = client.tags.find()[0]
+ updated_tag = last_tag.update(parent=parent_tag)
+ updated_tag = last_tag.update(parent_id=parent_tag["id"])
+ ```
+ """
+ ...
+
+
+
+class TagContentItems(ContextManager):
+ def __init__(self, ctx: Context, path: str) -> None:
+ ...
+
+ def find(self) -> list[ContentItem]:
+ """
+ Find all content items that are tagged with this tag.
+
+ Returns
+ -------
+ list[ContentItem]
+ List of content items that are tagged with this tag.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ first_tag = client.tags.find()[0]
+
+ first_tag_content_items = first_tag.content_items.find()
+ ```
+ """
+ ...
+
+
+
+class ChildTags(_RelatedTagsBase):
+ def __init__(self, ctx: Context, path: str, /, *, parent_tag: Tag) -> None:
+ ...
+
+ def find(self) -> list[Tag]:
+ """
+ Find all child tags that are direct children of a single tag.
+
+ Returns
+ -------
+ list[Tag]
+ List of child tags. (Does not include the parent tag.)
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ mytag = client.tags.get("TAG_ID_HERE")
+
+ child_tags = mytag.child_tags.find()
+ ```
+ """
+ ...
+
+
+
+class DescendantTags(_RelatedTagsBase):
+ def __init__(self, ctx: Context, /, *, parent_tag: Tag) -> None:
+ ...
+
+ def find(self) -> list[Tag]:
+ """
+ Find all child tags that descend from a single tag.
+
+ Returns
+ -------
+ list[Tag]
+ List of tags that descend from the parent tag.
+ """
+ ...
+
+
+
+class Tags(ContextManager):
+ """Content item tags resource."""
+ def __init__(self, ctx: Context, path: str) -> None:
+ ...
+
+ def get(self, tag_id: str) -> Tag:
+ """
+ Get a single tag by its identifier.
+
+ Parameters
+ ----------
+ tag_id : str
+ The identifier for the tag.
+
+ Returns
+ -------
+ Tag
+ The tag object.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ mytag = client.tags.get("TAG_ID_HERE")
+ ```
+ """
+ ...
+
+ @overload
+ def find(self, /, *, name: str = ..., parent: Tag = ...) -> list[Tag]:
+ ...
+
+ @overload
+ def find(self, /, *, name: str = ..., parent_id: str = ...) -> list[Tag]:
+ ...
+
+ def find(self, /, **kwargs) -> list[Tag]:
+ """
+ Find tags by name and/or parent.
+
+ Note: tag names are only unique within the scope of a parent, which means that it is
+ possible to have multiple results when querying by name; However, querying by both `name`
+ and `parent` ensures a single result.
+
+ Parameters
+ ----------
+ name : str, optional
+ The name of the tag.
+ parent : Tag, optional
+ The parent `Tag` object. If there is no parent, the tag is a top-level tag. Only one of
+ `parent` or `parent_id` can be provided.
+ parent_id : str, optional
+ The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
+
+ Returns
+ -------
+ list[Tag]
+ List of tags that match the query. Defaults to all Tags.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+
+ # Find all tags
+ all_tags = client.tags.find()
+
+ # Find all tags with the name
+ mytag = client.tags.find(name="tag_name")
+
+ # Find all tags with the name and parent
+ subtags = client.tags.find(name="sub_name", parent=mytag)
+ subtags = client.tags.find(name="sub_name", parent=mytag["id"])
+ ```
+ """
+ ...
+
+ @overload
+ def create(self, /, *, name: str) -> Tag:
+ ...
+
+ @overload
+ def create(self, /, *, name: str, parent: Tag) -> Tag:
+ ...
+
+ @overload
+ def create(self, /, *, name: str, parent_id: str) -> Tag:
+ ...
+
+ def create(self, /, **kwargs) -> Tag:
+ """
+ Create a tag.
+
+ Parameters
+ ----------
+ name : str
+ The name of the tag.
+ parent : Tag, optional
+ The parent `Tag` object. If there is no parent, the tag is a top-level tag. Only one of
+ `parent` or `parent_id` can be provided.
+ parent_id : str, optional
+ The identifier for the parent tag. If there is no parent, the tag is a top-level tag.
+
+ Returns
+ -------
+ Tag
+ Newly created tag object.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+
+ category_tag = client.tags.create(name="category_name")
+ tag = client.tags.create(name="tag_name", parent=category_tag)
+ ```
+ """
+ ...
+
+
+
+class ContentItemTags(ContextManager):
+ """Content item tags resource."""
+ def __init__(self, ctx: Context, path: str, /, *, tags_path: str, content_guid: str) -> None:
+ ...
+
+ def find(self) -> list[Tag]:
+ """
+ Find all tags that are associated with a single content item.
+
+ Returns
+ -------
+ list[Tag]
+ List of tags associated with the content item.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+ content_item = client.content.find_one()
+
+ # Find all tags associated with the content item
+ content_item_tags = content_item.tags.find()
+ ```
+ """
+ ...
+
+ def add(self, tag: str | Tag) -> None:
+ """
+ Add the specified tag to an individual content item.
+
+ When adding a tag, all tags above the specified tag in the tag tree are also added to the
+ content item.
+
+ Parameters
+ ----------
+ tag : str | Tag
+ The tag id or tag object to add to the content item.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+
+ content_item = client.content.find_one()
+ tag = client.tags.find()[0]
+
+ # Add a tag
+ content_item.tags.add(tag)
+ ```
+ """
+ ...
+
+ def delete(self, tag: str | Tag) -> None:
+ """
+ Remove the specified tag from an individual content item.
+
+ When removing a tag, all tags above the specified tag in the tag tree are also removed from
+ the content item.
+
+ Parameters
+ ----------
+ tag : str | Tag
+ The tag id or tag object to remove from the content item.
+
+ Examples
+ --------
+ ```python
+ import posit
+
+ client = posit.connect.Client()
+
+ content_item = client.content.find_one()
+ content_item_first_tag = content_item.tags.find()[0]
+
+ # Remove a tag
+ content_item.tags.delete(content_item_first_tag)
+ ```
+ """
+ ...
+
+
+
+from typing_extensions import overload
+from . import resources
+
+"""Task resources."""
+class Task(resources.BaseResource):
+ @property
+ def is_finished(self) -> bool:
+ """The task state.
+
+ If True, the task has completed. The task may have exited successfully
+ or have failed. Inspect the error_code to determine if the task finished
+ successfully or not.
+
+ Returns
+ -------
+ bool
+ """
+ ...
+
+ @property
+ def error_code(self) -> int | None:
+ """The error code.
+
+ The error code produced by the task. A non-zero value represent an
+ error. A zero value represents no error.
+
+ Returns
+ -------
+ int | None
+ Non-zero value indicates an error.
+ """
+ ...
+
+ @property
+ def error_message(self) -> str | None:
+ """The error message.
+
+ Returns
+ -------
+ str | None
+ Human readable error message, or None on success or not finished.
+ """
+ ...
+
+ @overload
+ def update(self, *args, first: int, wait: int, **kwargs) -> None:
+ """Update the task.
+
+ Parameters
+ ----------
+ first : int, default 0
+ Line to start output on.
+ wait : int, default 0
+ Maximum number of seconds to wait for the task to complete.
+ """
+ ...
+
+ @overload
+ def update(self, *args, **kwargs) -> None:
+ """Update the task."""
+ ...
+
+ def update(self, *args, **kwargs) -> None:
+ """Update the task.
+
+ See Also
+ --------
+ task.wait_for : Wait for the task to complete.
+
+ Notes
+ -----
+ When waiting for a task to complete, one should consider utilizing `task.wait_for`.
+
+ Examples
+ --------
+ >>> task.output
+ [
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+ ]
+ >>> task.update()
+ >>> task.output
+ [
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ "Pretium aenean pharetra magna ac placerat vestibulum lectus mauris."
+ ]
+ """
+ ...
+
+ def wait_for(self) -> None:
+ """Wait for the task to finish.
+
+ Examples
+ --------
+ >>> task.wait_for()
+ None
+ """
+ ...
+
+
+
+class Tasks(resources.Resources):
+ @overload
+ def get(self, *, uid: str, first: int, wait: int) -> Task:
+ """Get a task.
+
+ Parameters
+ ----------
+ uid : str
+ Task identifier.
+ first : int, default 0
+ Line to start output on.
+ wait : int, default 0
+ Maximum number of seconds to wait for the task to complete.
+
+ Returns
+ -------
+ Task
+ """
+ ...
+
+ @overload
+ def get(self, uid: str, **kwargs) -> Task:
+ """Get a task.
+
+ Parameters
+ ----------
+ uid : str
+ Task identifier.
+
+ Returns
+ -------
+ Task
+ """
+ ...
+
+ def get(self, uid: str, **kwargs) -> Task:
+ """Get a task.
+
+ Parameters
+ ----------
+ uid : str
+ Task identifier.
+
+ Returns
+ -------
+ Task
+ """
+ ...
+
+
+
+class Url(str):
+ """URL representation for Connect.
+
+ An opinionated URL representation of a Connect URL. Maintains various
+ conventions:
+ - It begins with a scheme.
+ - It is absolute.
+ - It contains '__api__'.
+
+ Supports Python builtin __add__ for append.
+
+ Methods
+ -------
+ append(path: str)
+ Append a path to the URL.
+
+ Examples
+ --------
+ >>> url = Url("http://connect.example.com/")
+ http://connect.example.com/__api__
+ >>> url + "endpoint"
+ http://connect.example.com/__api__/endpoint
+
+ Append works with string-like objects (e.g., objects that support casting to string)
+ >>> url = Url("http://connect.example.com/__api__/endpoint")
+ http://connect.example.com/__api__/endpoint
+ >>> url + 1
+ http://connect.example.com/__api__/endpoint/1
+ """
+ def __new__(cls, value: str): # -> Self:
+ ...
+
+ def __add__(self, path: str): # -> Url:
+ ...
+
+ def append(self, path: str) -> Url:
+ ...
+
+
+
+from typing_extensions import List, Literal, NotRequired, Required, TYPE_CHECKING, TypedDict, Unpack
+from .content import Content
+from .resources import BaseResource, Resources
+from .context import Context
+from .groups import Group
+
+"""User resources."""
+if TYPE_CHECKING:
+ ...
+class User(BaseResource):
+ @property
+ def content(self) -> Content:
+ ...
+
+ def lock(self, *, force: bool = ...): # -> None:
+ """
+ Lock the user account.
+
+ You cannot unlock your own account unless you have administrative privileges. Once an account is locked, only an admin can unlock it.
+
+ Parameters
+ ----------
+ force : bool, optional
+ If `True`, overrides lock protection allowing a user to lock their own account. Default is `False`.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ Lock another user's account:
+
+ >>> user.lock()
+
+ Attempt to lock your own account (will raise `RuntimeError` unless `force` is set to `True`):
+
+ >>> user.lock(force=True)
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/users/-guid-/lock
+ """
+ ...
+
+ def unlock(self): # -> None:
+ """
+ Unlock the user account.
+
+ This method unlocks the specified user's account. You must have administrative privileges to unlock accounts other than your own.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ Unlock a user's account:
+
+ >>> user.unlock()
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/users/-guid-/lock
+ """
+ ...
+
+ class UpdateUser(TypedDict):
+ """Update user request."""
+ email: NotRequired[str]
+ username: NotRequired[str]
+ first_name: NotRequired[str]
+ last_name: NotRequired[str]
+ user_role: NotRequired[Literal["administrator", "publisher", "viewer"]]
+ ...
+
+
+ def update(self, **kwargs: Unpack[UpdateUser]) -> None:
+ """
+ Update the user's attributes.
+
+ Parameters
+ ----------
+ email : str, not required
+ The new email address for the user. Default is `None`.
+ username : str, not required
+ The new username for the user. Default is `None`.
+ first_name : str, not required
+ The new first name for the user. Default is `None`.
+ last_name : str, not required
+ The new last name for the user. Default is `None`.
+ user_role : Literal["administrator", "publisher", "viewer"], not required
+ The new role for the user. Options are `'administrator'`, `'publisher'`, `'viewer'`. Default is `None`.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ Update the user's email and role:
+
+ >>> user.update(email="newemail@example.com", user_role="publisher")
+
+ Update the user's first and last name:
+
+ >>> user.update(first_name="Jane", last_name="Smith")
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#put-/v1/users/-guid-
+ """
+ ...
+
+ @property
+ def groups(self) -> UserGroups:
+ """
+ Retrieve the groups to which the user belongs.
+
+ Returns
+ -------
+ UserGroups
+ Helper class that returns the groups of which the user is a member.
+
+ Examples
+ --------
+ Retrieve the groups to which the user belongs:
+
+ ```python
+ user = client.users.get("USER_GUID_HERE")
+ groups = user.groups.find()
+ ```
+ """
+ ...
+
+
+
+class UserGroups(Resources):
+ def __init__(self, ctx: Context, user_guid: str) -> None:
+ ...
+
+ def add(self, group: str | Group) -> None:
+ """
+ Add the user to the specified group.
+
+ Parameters
+ ----------
+ group : str | Group
+ The group guid or `Group` object to which the user will be added.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+ user = client.users.get("USER_GUID_HERE")
+
+ # Add the user to the group
+ user.groups.add(group)
+
+ # Add the user to multiple groups
+ groups = [
+ client.groups.get("GROUP_GUID_1"),
+ client.groups.get("GROUP_GUID_2"),
+ ]
+ for group in groups:
+ user.groups.add(group)
+
+ # Add the user to a group by GUID
+ user.groups.add("GROUP_GUID_HERE")
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/groups/-group_guid-/members
+ """
+ ...
+
+ def delete(self, group: str | Group) -> None:
+ """
+ Remove the user from the specified group.
+
+ Parameters
+ ----------
+ group : str | Group
+ The group to which the user will be added.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ group = client.groups.get("GROUP_GUID_HERE")
+ user = client.users.get("USER_GUID_HERE")
+
+ # Remove the user from the group
+ user.groups.delete(group)
+
+ # Remove the user from multiple groups
+ groups = [
+ client.groups.get("GROUP_GUID_1"),
+ client.groups.get("GROUP_GUID_2"),
+ ]
+ for group in groups:
+ user.groups.delete(group)
+
+ # Remove the user from a group by GUID
+ user.groups.delete("GROUP_GUID_HERE")
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#delete-/v1/groups/-group_guid-/members/-user_guid-
+ """
+ ...
+
+ def find(self) -> List[Group]:
+ """
+ Retrieve the groups to which the user belongs.
+
+ Returns
+ -------
+ List[Group]
+ A list of groups to which the user belongs.
+
+ Examples
+ --------
+ ```python
+ from posit.connect import Client
+
+ client = Client("https://posit.example.com", "API_KEY")
+
+ user = client.users.get("USER_GUID_HERE")
+ groups = user.groups.find()
+ ```
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/groups/-group_guid-/members
+ """
+ ...
+
+
+
+class Users(Resources):
+ """Users resource."""
+ class CreateUser(TypedDict):
+ """Create user request."""
+ username: Required[str]
+ password: NotRequired[str]
+ user_must_set_password: NotRequired[bool]
+ email: NotRequired[str]
+ first_name: NotRequired[str]
+ last_name: NotRequired[str]
+ user_role: NotRequired[Literal["administrator", "publisher", "viewer"]]
+ unique_id: NotRequired[str]
+ ...
+
+
+ def create(self, **attributes: Unpack[CreateUser]) -> User:
+ """
+ Create a new user with the specified attributes.
+
+ Applies when server setting 'Authentication.Provider' is set to 'ldap', 'oauth2', 'pam', 'password', 'proxy', or 'saml'.
+
+ Parameters
+ ----------
+ username : str, required
+ The user's desired username.
+ password : str, not required
+ Applies when server setting 'Authentication.Provider="password"'. Cannot be set when `user_must_set_password` is `True`.
+ user_must_set_password : bool, not required
+ 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"'.
+ email : str, not required
+ The user's email address.
+ first_name : str, not required
+ The user's first name.
+ last_name : str, not required
+ The user's last name.
+ user_role : Literal["administrator", "publisher", "viewer"], not required
+ The user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Falls back to server setting 'Authorization.DefaultUserRole'.
+ unique_id : str, maybe required
+ Required when server is configured with SAML or OAuth2 (non-Google) authentication. Applies when server setting `ProxyAuth.UniqueIdHeader` is set.
+
+ Returns
+ -------
+ User
+ The newly created user.
+
+ Examples
+ --------
+ Create a user with a predefined password:
+
+ >>> user = client.create(
+ ... username="jdoe",
+ ... email="jdoe@example.com",
+ ... first_name="John",
+ ... last_name="Doe",
+ ... password="s3cur3p@ssword",
+ ... user_role="viewer",
+ ... )
+
+ Create a user who must set their own password:
+
+ >>> user = client.create(
+ ... username="jdoe",
+ ... email="jdoe@example.com",
+ ... first_name="John",
+ ... last_name="Doe",
+ ... user_must_set_password=True,
+ ... user_role="viewer",
+ ... )
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#post-/v1/users
+ """
+ ...
+
+ class FindUser(TypedDict):
+ """Find user request."""
+ prefix: NotRequired[str]
+ user_role: NotRequired[Literal["administrator", "publisher", "viewer"] | str]
+ account_status: NotRequired[Literal["locked", "licensed", "inactive"] | str]
+ ...
+
+
+ def find(self, **conditions: Unpack[FindUser]) -> List[User]:
+ """
+ Find users matching the specified conditions.
+
+ Parameters
+ ----------
+ prefix : str, not required
+ Filter users by prefix (username, first name, or last name). The filter is case-insensitive.
+ user_role : Literal["administrator", "publisher", "viewer"], not required
+ Filter by user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Use `'|'` to represent logical OR (e.g., `'viewer|publisher'`).
+ account_status : Literal["locked", "licensed", "inactive"], not required
+ 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.
+
+ Returns
+ -------
+ List[User]
+ A list of users matching the specified conditions.
+
+ Examples
+ --------
+ Find all users with a username, first name, or last name starting with 'jo':
+
+ >>> users = client.find(prefix="jo")
+
+ Find all users who are either viewers or publishers:
+
+ >>> users = client.find(user_role="viewer|publisher")
+
+ Find all users who are locked or licensed:
+
+ >>> users = client.find(account_status="locked|licensed")
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/users
+ """
+ ...
+
+ def find_one(self, **conditions: Unpack[FindUser]) -> User | None:
+ """
+ Find a user matching the specified conditions.
+
+ Parameters
+ ----------
+ prefix : str, optional
+ Filter users by prefix (username, first name, or last name). The filter is case-insensitive. Default is `None`.
+ user_role : Literal["administrator", "publisher", "viewer"], optional
+ Filter by user role. Options are `'administrator'`, `'publisher'`, `'viewer'`. Use `'|'` to represent logical OR (e.g., `'viewer|publisher'`). Default is `None`.
+ account_status : Literal["locked", "licensed", "inactive"], optional
+ 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`.
+
+ Returns
+ -------
+ User or None
+ The first user matching the specified conditions, or `None` if no user is found.
+
+ Examples
+ --------
+ Find a user with a username, first name, or last name starting with 'jo':
+
+ >>> user = client.find_one(prefix="jo")
+
+ Find a user who is either a viewer or publisher:
+
+ >>> user = client.find_one(user_role="viewer|publisher")
+
+ Find a user who is locked or licensed:
+
+ >>> user = client.find_one(account_status="locked|licensed")
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/users
+ """
+ ...
+
+ def get(self, uid: str) -> User:
+ """
+ Retrieve a user by their unique identifier (guid).
+
+ Parameters
+ ----------
+ uid : str
+ The unique identifier (guid) of the user to retrieve.
+
+ Returns
+ -------
+ User
+
+ Examples
+ --------
+ >>> user = client.get("123e4567-e89b-12d3-a456-426614174000")
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/users
+ """
+ ...
+
+ def count(self) -> int:
+ """
+ Return the total number of users.
+
+ Returns
+ -------
+ int
+
+ See Also
+ --------
+ * https://docs.posit.co/connect/api/#get-/v1/users
+ """
+ ...
+
+
+
+from typing_extensions import Callable, List, NotRequired, Optional, Required, TypedDict, Unpack
+from .context import Context
+from .resources import BaseResource, Resources
+
+"""Vanity URL resources."""
+class Vanity(BaseResource):
+ """A vanity resource.
+
+ Vanities maintain custom URL paths assigned to content.
+
+ Warnings
+ --------
+ Vanity paths may only contain alphanumeric characters, hyphens, underscores, and slashes.
+
+ Vanities cannot have children. For example, if the vanity path "/finance/" exists, the vanity path "/finance/budget/" cannot. But, if "/finance" does not exist, both "/finance/budget/" and "/finance/report" are allowed.
+
+ The following vanities are reserved by Connect:
+ - `/__`
+ - `/favicon.ico`
+ - `/connect`
+ - `/apps`
+ - `/users`
+ - `/groups`
+ - `/setpassword`
+ - `/user-completion`
+ - `/confirm`
+ - `/recent`
+ - `/reports`
+ - `/plots`
+ - `/unpublished`
+ - `/settings`
+ - `/metrics`
+ - `/tokens`
+ - `/help`
+ - `/login`
+ - `/welcome`
+ - `/register`
+ - `/resetpassword`
+ - `/content`
+ """
+ AfterDestroyCallback = Callable[[], None]
+ class VanityAttributes(TypedDict):
+ """Vanity attributes."""
+ path: Required[str]
+ content_guid: Required[str]
+ created_time: Required[str]
+ ...
+
+
+ def __init__(self, /, ctx: Context, *, after_destroy: Optional[AfterDestroyCallback] = ..., **kwargs: Unpack[VanityAttributes]) -> None:
+ """Initialize a Vanity.
+
+ Parameters
+ ----------
+ ctx : Context
+ after_destroy : AfterDestroyCallback, optional
+ Called after the Vanity is successfully destroyed, by default None
+ """
+ ...
+
+ def destroy(self) -> None:
+ """Destroy the vanity.
+
+ Raises
+ ------
+ ValueError
+ If the foreign unique identifier is missing or its value is `None`.
+
+ Warnings
+ --------
+ This operation is irreversible.
+
+ Note
+ ----
+ This action requires administrator privileges.
+ """
+ ...
+
+
+
+class Vanities(Resources):
+ """Manages a collection of vanities."""
+ def all(self) -> List[Vanity]:
+ """Retrieve all vanities.
+
+ Returns
+ -------
+ List[Vanity]
+
+ Notes
+ -----
+ This action requires administrator privileges.
+ """
+ ...
+
+
+
+class VanityMixin(BaseResource):
+ """Mixin class to add a vanity attribute to a resource."""
+ class HasGuid(TypedDict):
+ """Has a guid."""
+ guid: Required[str]
+ ...
+
+
+ def __init__(self, ctx: Context, **kwargs: Unpack[HasGuid]) -> None:
+ ...
+
+ @property
+ def vanity(self) -> Optional[str]:
+ """Get the vanity."""
+ ...
+
+ @vanity.setter
+ def vanity(self, value: str) -> None:
+ """Set the vanity.
+
+ Parameters
+ ----------
+ value : str
+ The vanity path.
+
+ Note
+ ----
+ This action requires owner or administrator privileges.
+
+ See Also
+ --------
+ create_vanity
+ """
+ ...
+
+ @vanity.deleter
+ def vanity(self) -> None:
+ """Destroy the vanity.
+
+ Warnings
+ --------
+ This operation is irreversible.
+
+ Note
+ ----
+ This action requires owner or administrator privileges.
+
+ See Also
+ --------
+ reset_vanity
+ """
+ ...
+
+ def reset_vanity(self) -> None:
+ """Unload the cached vanity.
+
+ Forces the next access, if any, to query the vanity from the Connect server.
+ """
+ ...
+
+ class CreateVanityRequest(TypedDict, total=False):
+ """A request schema for creating a vanity."""
+ path: Required[str]
+ force: NotRequired[bool]
+ ...
+
+
+ def create_vanity(self, **kwargs: Unpack[CreateVanityRequest]) -> Vanity:
+ """Create a vanity.
+
+ Parameters
+ ----------
+ path : str, required
+ The path for the vanity.
+ force : bool, not required
+ Whether to force the creation of the vanity. When True, any other vanity with the same path will be deleted.
+
+ Warnings
+ --------
+ If setting force=True, the destroy operation performed on the other vanity is irreversible.
+ """
+ ...
+
+ def find_vanity(self) -> Vanity:
+ """Find the vanity.
+
+ Returns
+ -------
+ Vanity
+ """
+ ...
+
+
+
+from typing_extensions import List
+from .context import Context
+from .resources import BaseResource, Resources
+from .tasks import Task
+
+class Variant(BaseResource):
+ def render(self) -> Task:
+ ...
+
+
+
+class Variants(Resources):
+ def __init__(self, ctx: Context, content_guid: str) -> None:
+ ...
+
+ def find(self) -> List[Variant]:
+ ...
+
+
+
+from . import connect as connect
+
+"""The Posit SDK."""
+
+
+
+from typing import Tuple, Union
+
+TYPE_CHECKING = ...
+if TYPE_CHECKING:
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
+else:
+ ...
+version: str
+__version__: str
+__version_tuple__: VERSION_TUPLE
+version_tuple: VERSION_TUPLE
+version = ...
+version_tuple = ...
+
+
+
+
+
+You are an assistant that can create Posit SDK python code that can provide code solutions to interact with the user's local Posit Connect instance.
+
+All of your answers need to be code based. When returning answers, please restate the question and then provide the code within a code block. Err on the side of simplicity in your code answers. Be ok with asking to increase or decrease the complexity.
+
+This is a serious exercise. Please provide evidence for each answer and double check the answers for accuracy. If a question cannot be answered using the materials and tools provided, please explicitly say so.
+
+If a question is unclear, please ask for clarification.
+
+If you feel there is an opportunity for further exploration, please suggest the prompts. Wrap each suggested prompt within a tag.
+
+If an answer can not be resolved, suggest to the user that they can explore calling these API routes themselves. Never produce code that calls these routes as we do not know the return type or successful status codes.
+
+API Routes:
+* GET /v1/tasks/{id} Get task details
+* GET /v1/experimental/groups/{guid}/content List content for which a group with given GUID has access to
+* GET /v1/groups List or search for group details
+* PUT /v1/groups Create a group using details from a remote authentication provider (LDAP)
+* POST /v1/groups Create a group from caller-supplied details (Password, PAM, OAuth2, SAML, Proxied)
+* GET /v1/groups/remote Search for group details from a remote provider
+* GET /v1/groups/{group_guid}/members Get group member details
+* POST /v1/groups/{group_guid}/members Add a group member
+* DELETE /v1/groups/{group_guid}/members/{user_guid} Remove a group member
+* GET /v1/groups/{guid} Get group details
+* POST /v1/groups/{guid} Modify a group name or owner (Password, PAM, OAuth2, SAML, Proxied)
+* DELETE /v1/groups/{guid} Delete a group
+* GET /v1/content/{guid}/bundles List bundles
+* POST /v1/content/{guid}/bundles Create a bundle by uploading an archive
+* GET /v1/content/{guid}/bundles/{id} Get bundle details
+* DELETE /v1/content/{guid}/bundles/{id} Delete bundle
+* GET /v1/content/{guid}/bundles/{id}/download Download the bundle archive
+* GET /v1/users/{guid}/keys List API keys
+* POST /v1/users/{guid}/keys Create an API key
+* GET /v1/users/{guid}/keys/{id} Get an API key
+* DELETE /v1/users/{guid}/keys/{id} Delete an API key
+* GET /v1/feature-usage Provides details about all tracked features.
+* GET /v1/system/checks List system check runs
+* POST /v1/system/checks Start a system check run
+* GET /v1/system/checks/{id} Get the status of a system check run
+* DELETE /v1/system/checks/{id} Delete a system check run
+* GET /v1/system/checks/{id}/results Get the results of a system check run
+* GET /v1/system/caches/runtime List runtime caches
+* DELETE /v1/system/caches/runtime Delete a runtime cache
+* GET /v1/system/offhost/service-accounts List Kubernetes service accounts
+* GET /v1/system/hosts List Connect hosts
+* GET /v1/content/{guid}/tags List tags for content
+* POST /v1/content/{guid}/tags Add tag to content
+* DELETE /v1/content/{guid}/tags/{id} Remove tag from content
+* GET /v1/tags List tags
+* POST /v1/tags Create tag
+* GET /v1/tags/{id} Get tag
+* DELETE /v1/tags/{id} Delete tag
+* PATCH /v1/tags/{id} Update tag
+* GET /v1/tags/{id}/content List content for tags
+* GET /v1/packages Get package dependencies for all content
+* GET /v1/content/{guid}/jobs Get jobs
+* GET /v1/content/{guid}/jobs/{key} Get job
+* DELETE /v1/content/{guid}/jobs/{key} Register job kill order
+* GET /v1/content/{guid}/jobs/{key}/download Download job log file
+* GET /v1/content/{guid}/jobs/{key}/error Get job error
+* GET /v1/content/{guid}/jobs/{key}/log Get job log
+* GET /v1/content/{guid}/jobs/{key}/tail Tail job log
+* GET /v1/content List content items
+* POST /v1/content Create content item
+* GET /v1/content/{guid} Get content details
+* DELETE /v1/content/{guid} Delete content
+* PATCH /v1/content/{guid} Update content
+* POST /v1/content/{guid}/build Build deployment bundle
+* POST /v1/content/{guid}/deploy Deploy deployment bundle
+* GET /v1/content/{guid}/environment Get environment variables
+* PUT /v1/content/{guid}/environment Set all environment variables
+* PATCH /v1/content/{guid}/environment Update environment variables
+* GET /v1/content/{guid}/oauth/integrations/associations List all OAuth integration associations for this content item.
+* PUT /v1/content/{guid}/oauth/integrations/associations Set all OAuth integration associations
+* GET /v1/content/{guid}/packages Get package dependencies
+* GET /v1/content/{guid}/repository Get Git repository
+* PUT /v1/content/{guid}/repository Set Git repository
+* DELETE /v1/content/{guid}/repository Remove Git repository location
+* PATCH /v1/content/{guid}/repository Update Git repository
+* PUT /v1/content/{guid}/thumbnail Set a content thumbnail
+* DELETE /v1/content/{guid}/thumbnail Delete a content thumbnail
+* GET /v1/oauth/templates List OAuth templates
+* GET /v1/oauth/templates/{key} Get OAuth template details
+* GET /v1/oauth/sessions List OAuth sessions
+* GET /v1/oauth/sessions/{guid} Get OAuth session details
+* DELETE /v1/oauth/sessions/{guid} Delete an OAuth session
+* GET /v1/oauth/integrations List OAuth integrations
+* POST /v1/oauth/integrations Create an OAuth integration
+* POST /v1/oauth/integrations/credentials Exchange Connect credentials for OAuth credentials
+* GET /v1/oauth/integrations/{guid} Get OAuth integration details
+* DELETE /v1/oauth/integrations/{guid} Delete an OAuth integration
+* PATCH /v1/oauth/integrations/{guid} Update an OAuth integration
+* GET /v1/oauth/integrations/{guid}/associations List all associations for this OAuth integration.
+* POST /v1/oauth/integrations/{guid}/verify Verify that an OAuth service account integration is configured correctly.
+* GET /v1/examples List examples
+* GET /v1/examples/{name}/thumbnail Get example thumbnail
+* GET /v1/examples/{name}/zip Download example
+* GET /v1/environments List execution environments
+* POST /v1/environments Create execution environment
+* GET /v1/environments/{guid} Get execution environment details
+* PUT /v1/environments/{guid} Update an execution environment
+* DELETE /v1/environments/{guid} Delete an execution environment
+* GET /v1/content/{guid}/permissions List permissions
+* POST /v1/content/{guid}/permissions Add permission
+* GET /v1/content/{guid}/permissions/{id} Get permission
+* PUT /v1/content/{guid}/permissions/{id} Update permission
+* DELETE /v1/content/{guid}/permissions/{id} Delete permission
+* GET /v1/audit_logs Get audit logs
+* GET /v1/instrumentation/content/visits Get Content Visits
+* GET /v1/instrumentation/shiny/usage Get Shiny App Usage
+* POST /v1/experimental/bootstrap Create first admininistrator and API key
+* POST /v1/bootstrap Create first admininistrator and API key
+* GET /v1/search/content Search for content items
+* GET /v1/user Get current user details
+* GET /v1/users List or search for user details
+* PUT /v1/users Create a user using details from a remote authentication provider (LDAP, OAuth2 with Google)
+* POST /v1/users Create a user from caller-supplied details (SAML, password, PAM, proxied, OAuth2 except with Google)
+* GET /v1/users/remote Search for user details from a remote provider
+* GET /v1/users/{guid} Get user details
+* PUT /v1/users/{guid} Update a user
+* POST /v1/users/{guid}/lock Lock a user
+* GET /v1/server_settings/python Get Python Information
+* GET /v1/server_settings/quarto Get Quarto Information
+* GET /v1/server_settings/r Get R Information
+* GET /v1/server_settings/tensorflow Get TensorFlow Information
+* GET /v1/timezones List Time Zones
+* GET /v1/content/{guid}/vanity Get vanity URL
+* PUT /v1/content/{guid}/vanity Set vanity URL
+* DELETE /v1/content/{guid}/vanity Delete vanity URL
+* GET /v1/vanities List vanity URLs
+
+
diff --git a/extensions/sdk-assistant/app.py b/extensions/sdk-assistant/app.py
new file mode 100644
index 0000000..5f49ec8
--- /dev/null
+++ b/extensions/sdk-assistant/app.py
@@ -0,0 +1,241 @@
+import os
+import pathlib
+import tempfile
+import urllib.parse
+
+import chatlas
+import faicons
+
+from shiny import App, Inputs, reactive, render, session, ui
+
+app_ui = ui.page_fillable(
+ ui.h1(
+ "SDK Assistant",
+ ui.input_action_link(
+ "info_link", label=None, icon=faicons.icon_svg("circle-info")
+ ),
+ ui.output_text("cost", inline=True),
+ ),
+ ui.output_ui("new_gh_issue", inline=True),
+ ui.chat_ui("chat", placeholder="Ask your posit-SDK questions here..."),
+ ui.tags.style(
+ """
+ #info_link {
+ font-size: medium;
+ vertical-align: super;
+ margin-left: 10px;
+ }
+ #cost {
+ color: lightgrey;
+ font-size: medium;
+ vertical-align: middle;
+ }
+ .sdk_suggested_prompt {
+ cursor: pointer;
+ border-radius: 0.5em;
+ display: list-item;
+ }
+ .external-link {
+ cursor: alias;
+ }
+ #new_gh_issue {
+ position: absolute;
+ right: 15px;
+ top: 15px;
+ height: 25px;
+ }
+ """
+ ),
+ ui.tags.script(
+ """
+ $(() => {
+ $("body").click(function(e) {
+ if (!$(e.target).hasClass("sdk_suggested_prompt")) {
+ return;
+ }
+ window.Shiny.setInputValue("new_sdk_prompt", $(e.target).text());
+ });
+ })
+ window.Shiny.addCustomMessageHandler("submit-chat", function(message) {
+ const enterEvent = new KeyboardEvent('keydown', {
+ key: 'Enter',
+ code: 'Enter',
+ keyCode: 13,
+ which: 13,
+ });
+
+ // Dispatch the 'Enter' event on the input element
+ console.log("Dispatching Enter event", message);
+ document.querySelector("#" + message['id'] + " textarea#chat_user_input").dispatchEvent(enterEvent);
+ });
+
+ """
+ ),
+ fillable_mobile=True,
+)
+
+
+def server(input: Inputs): # noqa: A002
+ aws_model = os.getenv("AWS_MODEL", "us.anthropic.claude-3-5-sonnet-20241022-v2:0")
+ aws_region = os.getenv("AWS_REGION", "us-east-1")
+ chat = chatlas.ChatBedrockAnthropic(model=aws_model, aws_region=aws_region)
+
+ prompt = pathlib.Path(__file__).parent / "_prompt.xml"
+ if not prompt.exists():
+ raise FileNotFoundError(
+ f"Prompt file not found: {prompt}; run `make shiny` to generate it."
+ )
+
+ chat.system_prompt = prompt.read_text()
+
+ chat_ui = ui.Chat("chat")
+
+ async def submit_chat(new_value: str):
+ chat_ui.update_user_input(value=new_value)
+
+ local_session = session.require_active_session(None)
+ await local_session.send_custom_message("submit-chat", {"id": "chat"})
+
+ @render.text
+ @reactive.event(chat_ui.messages)
+ def cost():
+ tokens = chat.tokens("cumulative")
+ if len(tokens) == 0:
+ return None
+
+ cost = sum(
+ [
+ # Input + Output
+ (token[0] * 0.003 / 1000.0) + (token[1] * 0.015 / 1000.0)
+ for token in tokens
+ if token is not None
+ ]
+ )
+ ans = "$%s" % float("%.3g" % cost)
+ while len(ans) < 5:
+ ans = ans + "0"
+ return ans
+
+ @render.ui
+ def new_gh_issue():
+ messages = chat_ui.messages()
+ for message in messages:
+ if message["role"] == "assistant":
+ break
+ else:
+ # No LLM response found. Return
+ return
+
+ first_message_content: str = str(messages[0].get("content", ""))
+
+ with tempfile.TemporaryDirectory() as tmpdirname:
+ export_path = pathlib.Path(tmpdirname) / "chat_export.md"
+ chat.export(export_path, include="all", include_system_prompt=False)
+
+ exported_content = export_path.read_text()
+
+ body = f"""
+**First message:**
+```
+{first_message_content}
+```
+
+**Desired outcome:**
+
+Please describe what you would like to achieve in `posit-sdk`. Any additional context, code, or examples are welcome!
+
+```python
+from posit.connect import Client
+client = Client()
+
+# Your code here
+```
+
+-----------------------------------------------
+
+
+Chat Log
+
+````markdown
+{exported_content}
+````
+
+"""
+
+ title = (
+ "SDK Assistant: `"
+ + (
+ first_message_content
+ if len(first_message_content) <= 50
+ else (first_message_content[:50] + "...")
+ )
+ + "`"
+ )
+
+ new_issue_url = (
+ "https://github.com/posit-dev/posit-sdk-py/issues/new?"
+ + urllib.parse.urlencode(
+ {
+ "title": title,
+ "labels": ["template idea"],
+ "body": body,
+ }
+ )
+ )
+
+ return ui.a(
+ ui.img(src="new_gh_issue.svg", alt="New GitHub Issue", height="100%"),
+ title="Submit script example to Posit SDK",
+ class_="external-link",
+ href=new_issue_url,
+ target="_blank",
+ )
+
+ @chat_ui.on_user_submit
+ async def _():
+ user_input = chat_ui.user_input()
+ if user_input is None:
+ return
+ await chat_ui.append_message_stream(
+ await chat.stream_async(
+ user_input,
+ echo="all",
+ )
+ )
+
+ @reactive.effect
+ @reactive.event(input.new_sdk_prompt)
+ async def _():
+ await submit_chat(input.new_sdk_prompt())
+
+ @reactive.effect
+ async def _init_chat_on_load():
+ await submit_chat(
+ "What are the pieces of Posit connect and how do they fit together?"
+ )
+
+ # Remove the effect after the first run
+ _init_chat_on_load.destroy()
+
+ @reactive.effect
+ @reactive.event(input.info_link)
+ async def _():
+ modal = ui.modal(
+ ui.h1("Information"),
+ ui.h3("Model"),
+ ui.pre(
+ f"Model: {aws_model}\nRegion: {aws_region}",
+ ),
+ ui.h3("System prompt"),
+ ui.pre(chat.system_prompt),
+ easy_close=True,
+ size="xl",
+ )
+ ui.modal_show(modal)
+
+
+app = App(
+ app_ui,
+ server,
+ static_assets=pathlib.Path(__file__).parent / "www",
+)
diff --git a/extensions/sdk-assistant/custom-prompt-instructions.md b/extensions/sdk-assistant/custom-prompt-instructions.md
new file mode 100644
index 0000000..f9d96c7
--- /dev/null
+++ b/extensions/sdk-assistant/custom-prompt-instructions.md
@@ -0,0 +1,9 @@
+You are an assistant that can create Posit SDK python code that can provide code solutions to interact with the user's local Posit Connect instance.
+
+All of your answers need to be code based. When returning answers, please restate the question and then provide the code within a code block. Err on the side of simplicity in your code answers. Be ok with asking to increase or decrease the complexity.
+
+This is a serious exercise. Please provide evidence for each answer and double check the answers for accuracy. If a question cannot be answered using the materials and tools provided, please explicitly say so.
+
+If a question is unclear, please ask for clarification.
+
+If you feel there is an opportunity for further exploration, please suggest the prompts. Wrap each suggested prompt within a tag.
diff --git a/extensions/sdk-assistant/manifest.json b/extensions/sdk-assistant/manifest.json
new file mode 100644
index 0000000..5ccac9b
--- /dev/null
+++ b/extensions/sdk-assistant/manifest.json
@@ -0,0 +1,30 @@
+{
+ "version": 1,
+ "locale": "en_US.UTF-8",
+ "metadata": {
+ "appmode": "python-shiny",
+ "entrypoint": "app"
+ },
+ "python": {
+ "version": "3.12.7",
+ "package_manager": {
+ "name": "pip",
+ "version": "24.3.1",
+ "package_file": "requirements.txt"
+ }
+ },
+ "files": {
+ "requirements.txt": {
+ "checksum": "f5f6e6c263f93a4c1c8e6f040af97a52"
+ },
+ "_prompt.xml": {
+ "checksum": "11b53de6fe16486696042838d0a64f86"
+ },
+ "app.py": {
+ "checksum": "0780643a8b9af200326f582a6f8b8be4"
+ },
+ "www/new_gh_issue.svg": {
+ "checksum": "0e700e135c5062d209ea19fb20725ea3"
+ }
+ }
+}
diff --git a/extensions/sdk-assistant/readme_access_granted.png b/extensions/sdk-assistant/readme_access_granted.png
new file mode 100644
index 0000000..e2e6024
Binary files /dev/null and b/extensions/sdk-assistant/readme_access_granted.png differ
diff --git a/extensions/sdk-assistant/repomix.config.json b/extensions/sdk-assistant/repomix.config.json
new file mode 100644
index 0000000..c45983c
--- /dev/null
+++ b/extensions/sdk-assistant/repomix.config.json
@@ -0,0 +1,25 @@
+{
+ "output": {
+ "style": "xml",
+ "headerText": "",
+ "instructionFilePath": "_repomix-instructions.md",
+ "fileSummary": true,
+ "directoryStructure": true,
+ "removeComments": false,
+ "removeEmptyLines": true,
+ "showLineNumbers": false,
+ "copyToClipboard": false,
+ "topFilesLength": 5,
+ "includeEmptyDirectories": false
+ },
+ "ignore": {
+ "useGitignore": false,
+ "useDefaultPatterns": true
+ },
+ "security": {
+ "enableSecurityCheck": true
+ },
+ "tokenCount": {
+ "encoding": "o200k_base"
+ }
+}
diff --git a/extensions/sdk-assistant/requirements.txt b/extensions/sdk-assistant/requirements.txt
new file mode 100644
index 0000000..90b6c7a
--- /dev/null
+++ b/extensions/sdk-assistant/requirements.txt
@@ -0,0 +1,4 @@
+anthropic[bedrock]
+chatlas @ git+https://github.com/posit-dev/chatlas@main
+faicons
+shiny
diff --git a/extensions/sdk-assistant/sdk-assistant.gif b/extensions/sdk-assistant/sdk-assistant.gif
new file mode 100644
index 0000000..8e07f87
Binary files /dev/null and b/extensions/sdk-assistant/sdk-assistant.gif differ
diff --git a/extensions/sdk-assistant/uv_test_chat.py b/extensions/sdk-assistant/uv_test_chat.py
new file mode 100644
index 0000000..4fa15f2
--- /dev/null
+++ b/extensions/sdk-assistant/uv_test_chat.py
@@ -0,0 +1,99 @@
+# /// script
+# requires-python = ">=3.12"
+# dependencies = [
+# "chatlas",
+# "anthropic[bedrock]",
+# ]
+#
+# [tool.uv.sources]
+# chatlas = { git = "https://github.com/posit-dev/chatlas", rev = "main" }
+# ///
+
+
+from __future__ import annotations
+
+import asyncio
+import os
+import pathlib
+
+import chatlas
+
+here = pathlib.Path(__file__).parent
+os.chdir(here)
+
+
+async def main() -> None:
+ print("Running chatlas")
+ aws_model = os.getenv("AWS_MODEL", "us.anthropic.claude-3-5-sonnet-20241022-v2:0")
+ aws_region = os.getenv("AWS_REGION", "us-east-1")
+ chat = chatlas.ChatBedrockAnthropic(model=aws_model, aws_region=aws_region)
+
+ chat.system_prompt = (here / "_prompt.xml").read_text()
+
+ prompt = "Which groups do I belong to?"
+ # Worked!
+
+ # prompt = "How many users have been active within the last 30 days."
+ # # Discovers that `Event`s sometimes do not have a user_guid associated with them.
+ # # Once adjusted to account for that, it works great!
+
+ # prompt = "Which python content items that I have deployed are using version 2 or later of the Requests package?"
+ # # Has a LOT of trouble finding "my" content items. All prompts currently return for all content items.
+ # # Even adjusting for `client.me.content.find()`, I run into a server error related to request:
+ # # GET v1/content/38be0f3e-9cca-4e7a-8ea8-0990d989786f/packages params={'language': 'python', 'name': 'requests'} response= response.content=b''
+ # # IDK why this one breaks and many others pass
+
+ # prompt = 'List all processes associated with "My Content".'
+ # # Worked great once I changed `content.jobs.find()` to `content.jobs.fetch()`!
+
+ # prompt = "Get me the group of users that publish the most quarto content in the last week"
+ # prompt = "Get me the set of users that published the most quarto content in the last 7 days"
+ # # Worked great!
+
+ ans = await chat.stream_async(prompt, echo="all")
+
+ content = await ans.get_content()
+ print("Content:\n", content)
+ print("--\n")
+
+ cost = sum(
+ [
+ # Input + Output
+ (token[0] * 0.003 / 1000.0) + (token[1] * 0.015 / 1000.0)
+ for token in chat.tokens("cumulative")
+ if token is not None
+ ]
+ )
+ # Example cost calculation:
+ # Link: https://aws.amazon.com/bedrock/pricing/#Pricing_details
+ # cost = 0.003 * input + 0.015 * output + 0.0003 * cached_input
+ # Our input is ~30k of the 35k
+ # Our output is < 500
+ # cost = 0.003 * (35 - 30) + 0.015 * 500/1000 + 0.0003 * 30
+ # cost = 0.0315
+ print("Token count: ", chat.tokens("cumulative"))
+ print("Cost: ", "$%s" % float("%.3g" % cost))
+
+ # Save to ./chatlas folder
+ pathlib.Path("chatlas").mkdir(exist_ok=True)
+ chat.export(here / "chatlas" / f"{prompt}.md", overwrite=True)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+
+ # Current output is 147k chars (29k tokens)
+ # Replacing all ` ` with ` ` would reduce it to 127k chars (~25k tokens)
+ # Bedrock Anthropic Claude states inputs to be less than 180k tokens.
+ # So, we are good to go.
+
+ # Next steps:
+ # √ 0. Token count
+ # √ 0. Prompts to try:
+ # * Which applications are using version 2 or later of the Requests package?
+ # * How many users have been active within the last 30 days.
+ # * List all processes associated with "My Content". (This functionality doesn't exist in the SDK, so see it makes up an answer)
+ # √ 1. prompt: "Get me the group of users that publish the most quarto content in the last week."
+ # * Possibly need to prompt about using Polars to help with intermediate steps.
+ # 1. Make app for demo
+ # 2. Add API routes, methods, descriptions and return types in a quick document
diff --git a/extensions/sdk-assistant/uv_update_prompt.py b/extensions/sdk-assistant/uv_update_prompt.py
new file mode 100644
index 0000000..801e424
--- /dev/null
+++ b/extensions/sdk-assistant/uv_update_prompt.py
@@ -0,0 +1,94 @@
+# /// script
+# requires-python = ">=3.12"
+# dependencies = [
+# "pyright",
+# "posit-sdk",
+# ]
+#
+# [tool.uv.sources]
+# chatlas = { git = "https://github.com/posit-dev/chatlas", rev = "main" }
+# posit-sdk = { git = "https://github.com/posit-dev/posit-sdk-py", rev = "main" }
+# ///
+from __future__ import annotations
+
+import asyncio
+import os
+import pathlib
+import shutil
+
+import pyright
+
+
+here = pathlib.Path(__file__).parent
+os.chdir(here)
+
+
+def cleanup() -> None:
+ # Clean slate
+ print("Clean up")
+ for f in [
+ "typings",
+ "_repomix-instructions.md",
+ ]:
+ path = here / f
+ if path.exists():
+ print("Removing path:", path.relative_to(here))
+ if path.is_file():
+ path.unlink()
+ else:
+ shutil.rmtree(path)
+ print("--\n")
+
+
+async def main() -> None:
+ # Clean slate
+ cleanup()
+
+ print("Creating type stubs: ./typings")
+ pyright.run("--createstub", "posit")
+ print("--\n")
+
+ print("Trimming type stubs")
+ remove_prefix_from_files(
+ "typings",
+ '"""\nThis type stub file was generated by pyright.\n"""\n\n',
+ )
+ print("--\n")
+
+ print("Getting Swagger information")
+ os.system("python ./_update_swagger.py")
+
+ with open(here / "_repomix-instructions.md", "w") as prompt_f:
+ prompt_f.write((here / "custom-prompt-instructions.md").read_text())
+ prompt_f.write("\n")
+ prompt_f.write((here / "_swagger_prompt.md").read_text())
+
+ print("--\n")
+
+ # repomix GitHub Repo: https://github.com/yamadashy/repomix
+ # Python alternative: https://pypi.org/project/code2prompt/
+ # * Does not contain XML output (suggested by anthropic)
+ print("Creating repomix output")
+ # Assert npx exists in system
+ assert os.system("npx --version") == 0, (
+ "npx not found in system. Please install Node.js"
+ )
+ exit_code = os.system(
+ "npx --package repomix --yes repomix --config repomix.config.json --output _prompt.xml typings/posit"
+ )
+ assert exit_code == 0, "repomix failed to build prompt file: _prompt.xml"
+ print("--\n")
+
+ # Clean slate
+ cleanup()
+
+
+def remove_prefix_from_files(folder: str | pathlib.Path, prefix: str) -> None:
+ root_folder = pathlib.Path(folder)
+ for path in root_folder.rglob("*.pyi"):
+ file_txt = path.read_text().removeprefix(prefix)
+ path.write_text(file_txt)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/extensions/sdk-assistant/uv_update_swagger.py b/extensions/sdk-assistant/uv_update_swagger.py
new file mode 100644
index 0000000..ff10116
--- /dev/null
+++ b/extensions/sdk-assistant/uv_update_swagger.py
@@ -0,0 +1,131 @@
+# /// script
+# requires-python = ">=3.12"
+# dependencies = [
+# "typing_extensions",
+# ]
+# ///
+from __future__ import annotations
+
+import pathlib
+import os
+import json
+from typing_extensions import TypedDict, TypeVar
+
+here = pathlib.Path(__file__).parent
+os.chdir(here)
+
+T = TypeVar("T")
+
+
+class SwaggerOperation(TypedDict, total=False):
+ summary: str
+
+
+class SwaggerDocument(TypedDict):
+ paths: dict[str, SwaggerOperation]
+
+
+class Route(TypedDict):
+ method: str
+ path: str
+ summary: str
+
+
+def transform_swagger_to_routes(
+ swagger_dict: SwaggerDocument,
+) -> list[Route]:
+ """
+ Swagger to routes.
+
+ Transforms the structure of a Swagger object to create a list where each object includes the route method, route path, and route summary.
+
+ Arguments
+ ---------
+ swagger_dict
+ The dictionary representing the Swagger document.
+
+ Returns
+ -------
+ :
+ A list of dictionaries where each dictionary includes the method, path, and summary of an API route.
+ """
+ routes: list[Route] = []
+
+ if "paths" not in swagger_dict:
+ raise ValueError(
+ "The Swagger document `swagger_dict=` does not contain a 'paths' key."
+ )
+
+ if "paths" in swagger_dict:
+ for path, operations in swagger_dict["paths"].items():
+ if not isinstance(path, str):
+ raise ValueError(
+ f"Expected route to be a string, but got {type(path)}."
+ )
+ for method, operation in operations.items():
+ if not isinstance(method, str):
+ raise ValueError(
+ f"Expected method to be a string, but got {type(method)}."
+ )
+ if not isinstance(operation, dict):
+ raise ValueError(
+ f"Expected operation to be a dictionary, but got {type(operation)}."
+ )
+ if "summary" not in operation:
+ raise ValueError(
+ f"Expected operation to have a 'summary' key, but got {operation}."
+ )
+
+ summary = operation["summary"]
+ if not isinstance(summary, str):
+ raise ValueError(
+ f"Expected summary to be a string, but got {type(summary)}."
+ )
+
+ routes.append(
+ {
+ "method": method,
+ "path": path,
+ "summary": summary,
+ }
+ )
+
+ return routes
+
+
+def main():
+ if not (here / "_swagger.json").exists():
+ import urllib.request
+
+ urllib.request.urlretrieve(
+ "https://docs.posit.co/connect/api/swagger.json",
+ here / "_swagger.json",
+ )
+
+ swagger = json.loads((here / "_swagger.json").read_text())
+
+ routes = transform_swagger_to_routes(swagger)
+
+ # Write out the swagger portion of the instructions with a preamble and a
+ # list of all the API routes and their short summaries.
+ with open(here / "_swagger_prompt.md", "w") as f:
+ f.write(
+ "If an answer can not be resolved, suggest to the user that they can explore calling these API routes themselves. Never produce code that calls these routes as we do not know the return type or successful status codes.\n\nAPI Routes:\n"
+ ""
+ )
+
+ for route in routes:
+ # `"* GET /v1/tasks/{id} Get task details"`
+ f.write(
+ "* "
+ + route["method"].upper()
+ + " "
+ + route["path"]
+ + " "
+ + route["summary"].replace("\n", " ").strip()
+ + "\n",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/extensions/sdk-assistant/www/new_gh_issue.svg b/extensions/sdk-assistant/www/new_gh_issue.svg
new file mode 100644
index 0000000..6743752
--- /dev/null
+++ b/extensions/sdk-assistant/www/new_gh_issue.svg
@@ -0,0 +1,13 @@
+