posit-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - main
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+ jobs:
11
+ lint:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v7
15
+ - uses: astral-sh/setup-uv@v9.0.0
16
+ - uses: extractions/setup-just@v4
17
+ # Fail fast if uv.lock has drifted from pyproject.toml.
18
+ - run: uv lock --locked
19
+ - run: just lint
20
+
21
+ test:
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ python-version:
27
+ - "3.8"
28
+ - "3.9"
29
+ - "3.10"
30
+ - "3.11"
31
+ - "3.12"
32
+ - "3.13"
33
+ - "3.14"
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: astral-sh/setup-uv@v9.0.0
37
+ with:
38
+ python-version: ${{ matrix.python-version }}
39
+ - uses: extractions/setup-just@v4
40
+ - run: just test ${{ matrix.python-version }}
41
+
42
+ build:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@v7
46
+ - uses: astral-sh/setup-uv@v9.0.0
47
+ - uses: extractions/setup-just@v4
48
+ - run: just build
49
+ - run: just smoke
@@ -0,0 +1,37 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags:
5
+ - "v*.*.*"
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ environment: release
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ with:
15
+ fetch-depth: 0
16
+ - uses: astral-sh/setup-uv@v9.0.0
17
+ - uses: extractions/setup-just@v4
18
+ - name: assert tag matches pyproject version
19
+ env:
20
+ TAG: ${{ github.ref_name }}
21
+ run: |
22
+ ver="$(just version)"
23
+ if [ "$TAG" != "v$ver" ]; then
24
+ echo "::error::tag '${TAG}' does not match pyproject version 'v${ver}'"
25
+ exit 1
26
+ fi
27
+ - name: assert tag is on main
28
+ env:
29
+ TAG: ${{ github.ref_name }}
30
+ run: |
31
+ if ! git merge-base --is-ancestor HEAD origin/main; then
32
+ echo "::error::tag '${TAG}' points to a commit that is not on main"
33
+ exit 1
34
+ fi
35
+ - run: just build
36
+ - run: just smoke
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,18 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+
8
+ # Virtual environments
9
+ .venv/
10
+ venv/
11
+
12
+ # Tooling
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .mypy_cache/
16
+ .coverage
17
+ # roborev snapshots
18
+ /.roborev/
@@ -0,0 +1,40 @@
1
+ # posit-cli
2
+
3
+ A single, friendly CLI — `posit` — for working with Posit Connect, in the spirit of
4
+ [`gh`](https://cli.github.com/). Distribution name `posit-cli`, import package `posit_cli`,
5
+ executable `posit`.
6
+
7
+ - `posit connect api <path>` is a `gh api`-style raw REST client (OAuth-authenticated).
8
+ - Other `posit connect` commands (`login`, `deploy`, `add`, `list`, ...) are mounted dynamically
9
+ from rsconnect-python's Click group, so they come for free and track upstream.
10
+
11
+ See `architecture-rsconnect-not-posit-sdk` in memory for why we wrap rsconnect-python (not
12
+ posit-sdk) and the OAuth-release gotcha.
13
+
14
+ ## Design principles
15
+
16
+ - **Idiomatic first; deviate from rsconnect-python when it helps.** We deliberately built a
17
+ separate project (not a patch to rsconnect) so we can make breaking changes freely. The CLI
18
+ surface is *ours*. When rsconnect-python's naming or behavior is awkward, prefer what
19
+ mainstream CLIs (especially `gh`, plus `curl`/`kubectl`) do, and what reads clearly to both
20
+ humans and coding agents. Don't stay coupled to rsconnect's details for their own sake.
21
+ - Example: rsconnect exposes `--insecure`/`-i` to skip TLS verification. We renamed it to
22
+ `--no-tls-verify` (more explicit; "insecure" is vague) and freed the `-i` short flag so it
23
+ can later mean `--include` as it does in `gh api`.
24
+ - **`posit connect api` tracks `gh api` conventions** where they make sense for Connect. Where a
25
+ `gh` idiom doesn't fit Connect (e.g. `{owner}/{repo}` path placeholders, GraphQL), drop it
26
+ rather than fake it.
27
+ - **Mounted rsconnect commands stay as-is** unless we have a specific reason to override one —
28
+ the value of mounting is that they track upstream. Overriding a name means owning it.
29
+
30
+ ## Conventions
31
+
32
+ - Keep credential flags consistent with the mounted rsconnect commands where they overlap
33
+ (`-n/--name`, `-s/--server`, `-k/--api-key`, env `CONNECT_SERVER`/`CONNECT_API_KEY`/
34
+ `CONNECT_INSECURE`) so the whole `posit connect` surface feels uniform — even when we rename
35
+ the flag, keep honoring the shared env var.
36
+ - `posit connect api` uses rsconnect's *internal* client (`RSConnectExecutor` ->
37
+ `RSConnectClient`), which has no stability contract. `tests/test_rsconnect_contract.py` guards
38
+ the surface we depend on; pin the rsconnect version and re-verify on bumps.
39
+
40
+ See `RELEASE.md` for how to cut a release to PyPI.
@@ -0,0 +1,38 @@
1
+ # posit-cli task runner. Run `just --list` to see recipes.
2
+
3
+ # Run the test suite against a single Python version (default 3.13)
4
+ test py="3.13":
5
+ uv run --python {{py}} --extra test pytest tests
6
+
7
+ # Check formatting and lint
8
+ lint:
9
+ uv run --extra lint ruff format --check
10
+ uv run --extra lint ruff check
11
+
12
+ # Auto-format and apply lint fixes
13
+ fmt:
14
+ uv run --extra lint ruff format
15
+ uv run --extra lint ruff check --fix
16
+
17
+ # Build wheel + sdist
18
+ build:
19
+ uv build
20
+
21
+ # Smoke-test the most recently built wheel (no project install)
22
+ smoke:
23
+ #!/usr/bin/env bash
24
+ set -euo pipefail
25
+ WHL=$(ls -t dist/*.whl | head -1)
26
+ uv run --no-project --with "$WHL" posit --help
27
+
28
+ # Install the most recently built wheel into the active environment
29
+ install: build
30
+ uv pip install "$(ls -t dist/*.whl | head -1)"
31
+
32
+ # Print the current version
33
+ version:
34
+ @uv version --short
35
+
36
+ # Remove build/test artifacts
37
+ clean:
38
+ rm -rf .coverage .pytest_cache .ruff_cache build dist *.egg-info
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Posit Software, PBC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: posit-cli
3
+ Version: 0.1.0
4
+ Summary: A single, friendly command-line interface for Posit Connect, in the spirit of gh.
5
+ Author: Posit Software, PBC
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.8
9
+ Requires-Dist: click>=8.0
10
+ Requires-Dist: jq>=1.4
11
+ Requires-Dist: keyring>=23.0
12
+ Requires-Dist: rsconnect-python<2,>=1.30
13
+ Provides-Extra: lint
14
+ Requires-Dist: ruff>=0.6; extra == 'lint'
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7; extra == 'test'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # posit-cli
20
+
21
+ A friendly command-line interface for Posit products, in the spirit of [`gh`](https://cli.github.com/).
22
+
23
+ ```console
24
+ $ posit connect login https://connect.example.com # OAuth, tokens in your OS keyring
25
+ $ posit connect api v1/user -q .username # gh-api-style raw request
26
+ $ posit connect deploy streamlit ./my-app # everything rsconnect can do
27
+ ```
28
+
29
+ This project is in early-stage development and so far only supports Posit Connect's APIs.
30
+
31
+ ## Installation
32
+
33
+ `posit-cli` isn't on PyPI yet. Install the latest version straight from GitHub
34
+ with [`uv`](https://docs.astral.sh/uv/):
35
+
36
+ ```console
37
+ uv tool install git+https://github.com/posit-dev/posit-cli.git
38
+ ```
39
+
40
+ If you authenticate to GitHub over SSH, use the `git+ssh://` form instead (uv
41
+ requires the `git@` username):
42
+
43
+ ```console
44
+ uv tool install git+ssh://git@github.com/posit-dev/posit-cli.git
45
+ ```
46
+
47
+ Either way this puts the `posit` executable on your `PATH`. To upgrade later, run
48
+ `uv tool upgrade posit-cli`. See uv's
49
+ [Git authentication docs](https://docs.astral.sh/uv/concepts/authentication/git/)
50
+ for tokens and other hosts.
51
+
52
+ ## Getting started
53
+
54
+ **1. Log in.** Point `posit` at your Connect server and authenticate. This runs
55
+ an OAuth 2.1 flow in your browser and saves the tokens in your OS keyring, so
56
+ you only do it once per server:
57
+
58
+ ```console
59
+ $ posit connect login https://connect.example.com
60
+ ```
61
+
62
+ **2. Try a request.** Confirm you're connected by asking Connect who you are:
63
+
64
+ ```console
65
+ $ posit connect api v1/user -q .username
66
+ ```
67
+
68
+ **3. Deploy something.** Anything `rsconnect` can deploy, `posit` can too:
69
+
70
+ ```console
71
+ $ posit connect deploy streamlit ./my-app
72
+ ```
73
+
74
+ That's it — from here, explore `posit connect --help` for the full command set.
75
+
76
+ ## Authentication
77
+
78
+ `posit connect login` runs an OAuth 2.1 flow and stores tokens in your OS
79
+ keyring; `posit connect api` and the deploy commands reuse those credentials
80
+ automatically (including token refresh). You can also point at a server ad hoc
81
+ with `--server`/`CONNECT_SERVER` and `--api-key`/`CONNECT_API_KEY`.
82
+
83
+ ## `posit connect api`
84
+
85
+ A `gh api`-style raw REST client, authenticated with your saved credentials.
86
+ `PATH` is relative to the Connect API root (the `/__api__` prefix is added for
87
+ you):
88
+
89
+ ```console
90
+ $ posit connect api v1/user # GET, pretty-printed JSON
91
+ $ posit connect api v1/user -q .username # filter with a jq expression
92
+ $ posit connect api "v1/content?limit=3" -q '.[].name'
93
+ ```
94
+
95
+ `-q`/`--jq` runs the response through [jq](https://jqlang.github.io/jq/), like
96
+ `gh api --jq`: string results print unquoted, one per line.
97
+
98
+ `-i`/`--include` prepends the HTTP status line and response headers to the
99
+ output (handy for inspecting rate limits, request IDs, or the server version):
100
+
101
+ ```console
102
+ $ posit connect api v1/user -i
103
+ ```
104
+
105
+ `--paginate` (GET only) follows Connect's pagination and prints all pages'
106
+ results combined. It handles both of Connect's pagination styles — the
107
+ cursor style (e.g. `v1/audit_logs`) and the page-number style (e.g.
108
+ `v1/users`, `v1/groups`) — and leaves unpaginated endpoints unchanged:
109
+
110
+ ```console
111
+ $ posit connect api v1/users --paginate -q 'length' # count every user
112
+ ```
113
+
114
+ `PATH` may also be a full URL on the configured server — convenient for pasting
115
+ a `paging.next` link straight back in. A URL for a different host is rejected.
116
+
117
+ ### Query parameters vs. request body
118
+
119
+ Like `gh api`, `-f`/`-F` fields default to a **POST request body** (adding any
120
+ field flips the method to POST unless you pass `-X`). To send **query
121
+ parameters** on a read, put them in the path or force `GET`:
122
+
123
+ ```console
124
+ $ posit connect api "v1/users?page_size=5" # query params in the path
125
+ $ posit connect api v1/users -X GET -f page_size=5 # or force GET
126
+ $ posit connect api v1/content -f name=my-app # POST body (creates content)
127
+ ```
128
+
129
+ ## `posit connect *`
130
+
131
+ `posit` wraps [`rsconnect-python`](https://github.com/posit-dev/rsconnect-python):
132
+ it re-exposes the full `rsconnect` command set under `posit connect`, in addition to the `api` utility.
133
+
134
+ ## Using `posit` with Claude Code
135
+
136
+ This repo ships a skill (at [`skills/posit-cli/`](skills/posit-cli/)) that teaches
137
+ coding agents to drive `posit` — auth, `posit connect api`, and deploys. Install
138
+ it with the [`skills`](https://www.skills.sh/) CLI:
139
+
140
+ ```console
141
+ npx skills add posit-dev/posit-cli
142
+ ```
@@ -0,0 +1,124 @@
1
+ # posit-cli
2
+
3
+ A friendly command-line interface for Posit products, in the spirit of [`gh`](https://cli.github.com/).
4
+
5
+ ```console
6
+ $ posit connect login https://connect.example.com # OAuth, tokens in your OS keyring
7
+ $ posit connect api v1/user -q .username # gh-api-style raw request
8
+ $ posit connect deploy streamlit ./my-app # everything rsconnect can do
9
+ ```
10
+
11
+ This project is in early-stage development and so far only supports Posit Connect's APIs.
12
+
13
+ ## Installation
14
+
15
+ `posit-cli` isn't on PyPI yet. Install the latest version straight from GitHub
16
+ with [`uv`](https://docs.astral.sh/uv/):
17
+
18
+ ```console
19
+ uv tool install git+https://github.com/posit-dev/posit-cli.git
20
+ ```
21
+
22
+ If you authenticate to GitHub over SSH, use the `git+ssh://` form instead (uv
23
+ requires the `git@` username):
24
+
25
+ ```console
26
+ uv tool install git+ssh://git@github.com/posit-dev/posit-cli.git
27
+ ```
28
+
29
+ Either way this puts the `posit` executable on your `PATH`. To upgrade later, run
30
+ `uv tool upgrade posit-cli`. See uv's
31
+ [Git authentication docs](https://docs.astral.sh/uv/concepts/authentication/git/)
32
+ for tokens and other hosts.
33
+
34
+ ## Getting started
35
+
36
+ **1. Log in.** Point `posit` at your Connect server and authenticate. This runs
37
+ an OAuth 2.1 flow in your browser and saves the tokens in your OS keyring, so
38
+ you only do it once per server:
39
+
40
+ ```console
41
+ $ posit connect login https://connect.example.com
42
+ ```
43
+
44
+ **2. Try a request.** Confirm you're connected by asking Connect who you are:
45
+
46
+ ```console
47
+ $ posit connect api v1/user -q .username
48
+ ```
49
+
50
+ **3. Deploy something.** Anything `rsconnect` can deploy, `posit` can too:
51
+
52
+ ```console
53
+ $ posit connect deploy streamlit ./my-app
54
+ ```
55
+
56
+ That's it — from here, explore `posit connect --help` for the full command set.
57
+
58
+ ## Authentication
59
+
60
+ `posit connect login` runs an OAuth 2.1 flow and stores tokens in your OS
61
+ keyring; `posit connect api` and the deploy commands reuse those credentials
62
+ automatically (including token refresh). You can also point at a server ad hoc
63
+ with `--server`/`CONNECT_SERVER` and `--api-key`/`CONNECT_API_KEY`.
64
+
65
+ ## `posit connect api`
66
+
67
+ A `gh api`-style raw REST client, authenticated with your saved credentials.
68
+ `PATH` is relative to the Connect API root (the `/__api__` prefix is added for
69
+ you):
70
+
71
+ ```console
72
+ $ posit connect api v1/user # GET, pretty-printed JSON
73
+ $ posit connect api v1/user -q .username # filter with a jq expression
74
+ $ posit connect api "v1/content?limit=3" -q '.[].name'
75
+ ```
76
+
77
+ `-q`/`--jq` runs the response through [jq](https://jqlang.github.io/jq/), like
78
+ `gh api --jq`: string results print unquoted, one per line.
79
+
80
+ `-i`/`--include` prepends the HTTP status line and response headers to the
81
+ output (handy for inspecting rate limits, request IDs, or the server version):
82
+
83
+ ```console
84
+ $ posit connect api v1/user -i
85
+ ```
86
+
87
+ `--paginate` (GET only) follows Connect's pagination and prints all pages'
88
+ results combined. It handles both of Connect's pagination styles — the
89
+ cursor style (e.g. `v1/audit_logs`) and the page-number style (e.g.
90
+ `v1/users`, `v1/groups`) — and leaves unpaginated endpoints unchanged:
91
+
92
+ ```console
93
+ $ posit connect api v1/users --paginate -q 'length' # count every user
94
+ ```
95
+
96
+ `PATH` may also be a full URL on the configured server — convenient for pasting
97
+ a `paging.next` link straight back in. A URL for a different host is rejected.
98
+
99
+ ### Query parameters vs. request body
100
+
101
+ Like `gh api`, `-f`/`-F` fields default to a **POST request body** (adding any
102
+ field flips the method to POST unless you pass `-X`). To send **query
103
+ parameters** on a read, put them in the path or force `GET`:
104
+
105
+ ```console
106
+ $ posit connect api "v1/users?page_size=5" # query params in the path
107
+ $ posit connect api v1/users -X GET -f page_size=5 # or force GET
108
+ $ posit connect api v1/content -f name=my-app # POST body (creates content)
109
+ ```
110
+
111
+ ## `posit connect *`
112
+
113
+ `posit` wraps [`rsconnect-python`](https://github.com/posit-dev/rsconnect-python):
114
+ it re-exposes the full `rsconnect` command set under `posit connect`, in addition to the `api` utility.
115
+
116
+ ## Using `posit` with Claude Code
117
+
118
+ This repo ships a skill (at [`skills/posit-cli/`](skills/posit-cli/)) that teaches
119
+ coding agents to drive `posit` — auth, `posit connect api`, and deploys. Install
120
+ it with the [`skills`](https://www.skills.sh/) CLI:
121
+
122
+ ```console
123
+ npx skills add posit-dev/posit-cli
124
+ ```
@@ -0,0 +1,22 @@
1
+ # Releasing
2
+
3
+ Follow these steps to publish a new version to PyPI:
4
+
5
+ 1. Bump `version` in `pyproject.toml`.
6
+ 2. Run `uv lock` if the bump changed any dependency. CI fails the build if `uv.lock` has drifted.
7
+ 3. Commit the change and merge it to `main`.
8
+ 4. Tag the merged commit on `main` as `vX.Y.Z`. The tag must match the `pyproject.toml` version
9
+ exactly.
10
+ 5. Push the tag. This triggers `.github/workflows/release.yaml`.
11
+
12
+ The release workflow checks the tag against `pyproject.toml` and against `main`, builds the
13
+ wheel and sdist, smoke-tests the wheel, then publishes to PyPI through GitHub's OIDC
14
+ trusted-publisher flow. No PyPI token is stored in this repo.
15
+
16
+ ## One-time setup for a new repo
17
+
18
+ - Create a `release` environment under the repo's Settings > Environments, with required
19
+ reviewers. The `publish` job runs under this environment; without required reviewers, any tag
20
+ push publishes to PyPI immediately with no human gate.
21
+ - Configure a trusted publisher for `posit-cli` on PyPI with: Owner `posit-dev`, Repository name
22
+ `posit-cli`, Workflow name `release.yaml`, Environment name `release`.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "posit-cli"
3
+ version = "0.1.0"
4
+ description = "A single, friendly command-line interface for Posit Connect, in the spirit of gh."
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "Posit Software, PBC" }]
10
+ dependencies = [
11
+ # posit-cli reuses rsconnect-python's *internal* API (RSConnectExecutor,
12
+ # RSConnectClient). These have no stability contract, so re-verify the internals
13
+ # on bumps. See tests/test_rsconnect_contract.py.
14
+ "rsconnect-python>=1.30,<2",
15
+ # rsconnect imports `keyring` optionally; we depend on it directly so OAuth
16
+ # tokens land in the OS keyring.
17
+ "keyring>=23.0",
18
+ "click>=8.0",
19
+ # jq bindings power `posit connect api --jq`, mirroring `gh api --jq`.
20
+ "jq>=1.4",
21
+ ]
22
+
23
+ [project.scripts]
24
+ posit = "posit_cli.__main__:cli"
25
+
26
+ [project.optional-dependencies]
27
+ test = ["pytest>=7"]
28
+ lint = ["ruff>=0.6"]
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/posit_cli"]
36
+
37
+ [tool.ruff]
38
+ line-length = 99
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E4", "E7", "E9", "F"]