cytario-cli 1.0.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.
- cytario_cli-1.0.0/.github/workflows/ci.yml +61 -0
- cytario_cli-1.0.0/.github/workflows/enforce-semi-linear.yml +16 -0
- cytario_cli-1.0.0/.github/workflows/release.yml +139 -0
- cytario_cli-1.0.0/.gitignore +21 -0
- cytario_cli-1.0.0/AGENTS.md +32 -0
- cytario_cli-1.0.0/LICENSE +21 -0
- cytario_cli-1.0.0/PKG-INFO +99 -0
- cytario_cli-1.0.0/README.md +70 -0
- cytario_cli-1.0.0/pyproject.toml +123 -0
- cytario_cli-1.0.0/skills/cytario-cli.md +50 -0
- cytario_cli-1.0.0/src/cytario_cli/__init__.py +7 -0
- cytario_cli-1.0.0/src/cytario_cli/__main__.py +8 -0
- cytario_cli-1.0.0/src/cytario_cli/api.py +26 -0
- cytario_cli-1.0.0/src/cytario_cli/awsconfig.py +86 -0
- cytario_cli-1.0.0/src/cytario_cli/cli.py +257 -0
- cytario_cli-1.0.0/src/cytario_cli/config.py +75 -0
- cytario_cli-1.0.0/src/cytario_cli/oidc.py +214 -0
- cytario_cli-1.0.0/tests/test_api.py +68 -0
- cytario_cli-1.0.0/tests/test_awsconfig.py +97 -0
- cytario_cli-1.0.0/tests/test_config.py +39 -0
- cytario_cli-1.0.0/tests/test_oidc.py +78 -0
- cytario_cli-1.0.0/uv.lock +1013 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
env:
|
|
14
|
+
TZ: "Europe/Berlin"
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
lint-and-test:
|
|
18
|
+
name: Lint, format check & test
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
timeout-minutes: 5
|
|
21
|
+
permissions:
|
|
22
|
+
contents: read
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout repository
|
|
25
|
+
uses: actions/checkout@v5
|
|
26
|
+
|
|
27
|
+
- name: Setup uv
|
|
28
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
29
|
+
with:
|
|
30
|
+
enable-cache: true
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync
|
|
34
|
+
|
|
35
|
+
- name: Check formatting
|
|
36
|
+
id: format_step
|
|
37
|
+
run: uv run ruff format --check
|
|
38
|
+
|
|
39
|
+
- name: Run linter
|
|
40
|
+
id: lint_step
|
|
41
|
+
run: uv run ruff check
|
|
42
|
+
|
|
43
|
+
- name: Run tests
|
|
44
|
+
id: test_step
|
|
45
|
+
run: uv run pytest
|
|
46
|
+
|
|
47
|
+
- name: Build package
|
|
48
|
+
id: build_step
|
|
49
|
+
run: uv build
|
|
50
|
+
|
|
51
|
+
- name: Generate Summary
|
|
52
|
+
if: always()
|
|
53
|
+
run: |
|
|
54
|
+
echo "## CI Summary" >> $GITHUB_STEP_SUMMARY
|
|
55
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
56
|
+
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
|
57
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
58
|
+
echo "- Ruff format: ${{ steps.format_step.outcome }}" >> $GITHUB_STEP_SUMMARY
|
|
59
|
+
echo "- Ruff check: ${{ steps.lint_step.outcome }}" >> $GITHUB_STEP_SUMMARY
|
|
60
|
+
echo "- Pytest: ${{ steps.test_step.outcome }}" >> $GITHUB_STEP_SUMMARY
|
|
61
|
+
echo "- uv build: ${{ steps.build_step.outcome }}" >> $GITHUB_STEP_SUMMARY
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Enforce Semi-Linear History
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
check-linear-branch:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
with:
|
|
13
|
+
fetch-depth: 0
|
|
14
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
15
|
+
|
|
16
|
+
- uses: cytario/enforce-semi-linear-history@v1
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Cuts a new release on every push to `main` that warrants one, using
|
|
4
|
+
# python-semantic-release driven by Conventional Commits — WITHOUT creating a
|
|
5
|
+
# new commit on `main` (org policy forbids that). Instead:
|
|
6
|
+
#
|
|
7
|
+
# `semantic-release version --no-commit --no-changelog` (single command) does
|
|
8
|
+
# everything:
|
|
9
|
+
# * stamps the next version into pyproject.toml /
|
|
10
|
+
# src/cytario_cli/__init__.py (in the working tree, uncommitted),
|
|
11
|
+
# * runs `uv build` (the build_command) → sdist + wheel,
|
|
12
|
+
# * creates the tag `v<x>` locally at HEAD and pushes it to the remote via
|
|
13
|
+
# git. With `--no-commit` there is no release commit, so PSR pushes ONLY
|
|
14
|
+
# the tag refspec — nothing is pushed to refs/heads/main. A real git tag
|
|
15
|
+
# ends up in the repo (discoverable via `git tag` / `git describe`),
|
|
16
|
+
# * creates the GitHub Release for that tag and uploads the built dists as
|
|
17
|
+
# release assets.
|
|
18
|
+
#
|
|
19
|
+
# A separate `publish` job then uploads the same dists to PyPI via trusted
|
|
20
|
+
# publishing (OIDC) — no API token is stored in the repo. The first release
|
|
21
|
+
# needs the project registered as a PyPI trusted publisher (CONTRIBUTING.md).
|
|
22
|
+
#
|
|
23
|
+
# Release notes are generated from the Conventional Commits history and
|
|
24
|
+
# attached to the GitHub Release — no CHANGELOG.md file is maintained on the
|
|
25
|
+
# branch.
|
|
26
|
+
#
|
|
27
|
+
# PSR is run via `uv run` (not the wrapper action) so the `uv build`
|
|
28
|
+
# build_command runs on the host where `uv` is on PATH; the wrapper action
|
|
29
|
+
# runs PSR in a Docker container that does not contain `uv`.
|
|
30
|
+
|
|
31
|
+
on:
|
|
32
|
+
push:
|
|
33
|
+
branches: [main]
|
|
34
|
+
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
|
|
38
|
+
jobs:
|
|
39
|
+
release:
|
|
40
|
+
name: Semantic release
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
timeout-minutes: 15
|
|
43
|
+
concurrency:
|
|
44
|
+
group: ${{ github.workflow }}-release-${{ github.ref_name }}
|
|
45
|
+
cancel-in-progress: false
|
|
46
|
+
permissions:
|
|
47
|
+
contents: write # push tag + create GitHub release
|
|
48
|
+
outputs:
|
|
49
|
+
released: ${{ steps.release.outputs.released }}
|
|
50
|
+
version: ${{ steps.release.outputs.version }}
|
|
51
|
+
tag: ${{ steps.release.outputs.tag }}
|
|
52
|
+
steps:
|
|
53
|
+
- name: Checkout repository
|
|
54
|
+
uses: actions/checkout@v5
|
|
55
|
+
with:
|
|
56
|
+
fetch-depth: 0
|
|
57
|
+
# PSR injects the GH_TOKEN into the git remote URL itself for the tag
|
|
58
|
+
# push, so we don't need checkout to persist credentials.
|
|
59
|
+
persist-credentials: false
|
|
60
|
+
|
|
61
|
+
# Pull tags created by previous runs so PSR can see the last released
|
|
62
|
+
# version (tags are pushed via git, not just GitHub Releases).
|
|
63
|
+
- name: Fetch tags
|
|
64
|
+
run: git fetch --tags --force origin
|
|
65
|
+
|
|
66
|
+
- name: Setup uv
|
|
67
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
68
|
+
with:
|
|
69
|
+
enable-cache: true
|
|
70
|
+
|
|
71
|
+
- name: Install dependencies
|
|
72
|
+
run: uv sync
|
|
73
|
+
|
|
74
|
+
# `version --no-commit --no-changelog` stamps the version, builds, creates
|
|
75
|
+
# + pushes the tag (only the tag — no commit, no branch push), and creates
|
|
76
|
+
# the GitHub Release with the built dists as assets. PSR writes
|
|
77
|
+
# `released`/`version`/`tag` to $GITHUB_OUTPUT automatically.
|
|
78
|
+
- name: Semantic Release
|
|
79
|
+
id: release
|
|
80
|
+
env:
|
|
81
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
82
|
+
run: uv run semantic-release version --no-commit --no-changelog
|
|
83
|
+
|
|
84
|
+
- name: Upload distribution artifacts
|
|
85
|
+
if: steps.release.outputs.released == 'true'
|
|
86
|
+
uses: actions/upload-artifact@v4
|
|
87
|
+
with:
|
|
88
|
+
name: dist
|
|
89
|
+
path: dist/
|
|
90
|
+
if-no-files-found: error
|
|
91
|
+
retention-days: 7
|
|
92
|
+
|
|
93
|
+
- name: Generate Summary
|
|
94
|
+
if: always()
|
|
95
|
+
run: |
|
|
96
|
+
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
|
|
97
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
98
|
+
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
|
99
|
+
if [ "${{ steps.release.outputs.released }}" == "true" ]; then
|
|
100
|
+
echo "- **Released**: ✓ ${{ steps.release.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
|
101
|
+
echo "- **GitHub**: https://github.com/${{ github.repository }}/releases/tag/${{ steps.release.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
|
102
|
+
else
|
|
103
|
+
echo "- **Released**: ✗ No changes warranting a release" >> $GITHUB_STEP_SUMMARY
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
publish:
|
|
107
|
+
name: Publish to PyPI
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
timeout-minutes: 10
|
|
110
|
+
needs: release
|
|
111
|
+
if: needs.release.outputs.released == 'true'
|
|
112
|
+
environment:
|
|
113
|
+
name: pypi
|
|
114
|
+
url: https://pypi.org/project/cytario-cli/
|
|
115
|
+
permissions:
|
|
116
|
+
id-token: write
|
|
117
|
+
contents: read
|
|
118
|
+
steps:
|
|
119
|
+
- name: Download distribution artifacts
|
|
120
|
+
uses: actions/download-artifact@v4
|
|
121
|
+
with:
|
|
122
|
+
name: dist
|
|
123
|
+
path: dist
|
|
124
|
+
|
|
125
|
+
- name: Publish to PyPI
|
|
126
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
127
|
+
with:
|
|
128
|
+
packages-dir: dist
|
|
129
|
+
print-hash: true
|
|
130
|
+
verbose: true
|
|
131
|
+
|
|
132
|
+
- name: Generate Summary
|
|
133
|
+
if: always()
|
|
134
|
+
run: |
|
|
135
|
+
echo "## PyPI Publish Summary" >> $GITHUB_STEP_SUMMARY
|
|
136
|
+
echo "" >> $GITHUB_STEP_SUMMARY
|
|
137
|
+
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
|
138
|
+
echo "- **Version**: ${{ needs.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
139
|
+
echo "- **PyPI**: https://pypi.org/project/cytario-cli/${{ needs.release.outputs.version }}/" >> $GITHUB_STEP_SUMMARY
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.egg-info/
|
|
6
|
+
*.egg
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# Tooling caches
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.uv-cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# AGENTS.md — cytario-cli
|
|
2
|
+
|
|
3
|
+
CLI for working with Cytario storage connections as the signed-in user.
|
|
4
|
+
Python ≥ 3.10, Typer, uv, hatchling. Published to public PyPI as `cytario-cli`.
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv sync # install deps
|
|
10
|
+
uv run ruff format --check # format gate
|
|
11
|
+
uv run ruff check # lint gate (ALL, see pyproject ignore list)
|
|
12
|
+
uv run pytest # test gate
|
|
13
|
+
uv run cytario --help # run the CLI from the working tree
|
|
14
|
+
uv build # sdist + wheel
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Conventions
|
|
18
|
+
|
|
19
|
+
- Conventional Commits; releases cut by python-semantic-release on push to
|
|
20
|
+
main (no release commits — tag + GitHub Release only; PyPI via trusted
|
|
21
|
+
publishing in the `pypi` environment).
|
|
22
|
+
- npm-style comment discipline as the rest of Cytario: minimal comments, no
|
|
23
|
+
ticket IDs in code, no history comments.
|
|
24
|
+
- The skill file `skills/cytario-cli.md` documents the agent workflow; keep it
|
|
25
|
+
in sync with the CLI's commands.
|
|
26
|
+
- Security invariants (do not weaken):
|
|
27
|
+
- public client only — no client secret is ever shipped or stored;
|
|
28
|
+
- the refresh grant and token files are `0600` under `~/.config/cytario/cli`
|
|
29
|
+
and `~/.aws/cytario/`;
|
|
30
|
+
- no long-lived AWS keys are written; profiles use `web_identity_token_file`;
|
|
31
|
+
- the CLI never proxies data — the workstation's AWS tooling talks to the
|
|
32
|
+
storage provider directly under the user's own grant.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cytario
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cytario-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI for working with Cytario storage connections as the signed-in user (browser OIDC sign-in, AWS CLI profile setup, token refresh)
|
|
5
|
+
Project-URL: Homepage, https://github.com/cytario/cytario-cli
|
|
6
|
+
Project-URL: Repository, https://github.com/cytario/cytario-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/cytario/cytario-cli/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cytario/cytario-cli/releases
|
|
9
|
+
Author: Cytario
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agent,aws,cytario,oidc,sts,web-identity
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx>=0.27
|
|
27
|
+
Requires-Dist: typer>=0.12
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# cytario-cli
|
|
31
|
+
|
|
32
|
+
CLI for working with [Cytario](https://github.com/cytario) storage connections
|
|
33
|
+
as the signed-in user — designed for AI agents and analysts who need to work
|
|
34
|
+
with connection data using the workstation's standard AWS tooling.
|
|
35
|
+
|
|
36
|
+
## What it does
|
|
37
|
+
|
|
38
|
+
- **`cytario auth login --host <url>`** — browser sign-in (OAuth 2.0
|
|
39
|
+
Authorization Code + PKCE against the deployment's identity service; a
|
|
40
|
+
loopback redirect receives the result). The refresh grant is stored
|
|
41
|
+
user-private (`~/.config/cytario/cli`, `0600`).
|
|
42
|
+
- **`cytario connections list [--json]`** — the connections visible to you,
|
|
43
|
+
with the storage role and access level your grant resolves to.
|
|
44
|
+
- **`cytario connections setup [--all | <name>]`** — writes one AWS CLI profile
|
|
45
|
+
per connection into `~/.aws/config` (`[profile cytario-<name>]`) with
|
|
46
|
+
`web_identity_token_file`, so `aws` / boto3 / pandas perform
|
|
47
|
+
`AssumeRoleWithWebIdentity` themselves — with exactly your grant's
|
|
48
|
+
authorization, never wider.
|
|
49
|
+
- **`cytario auth token` / `cytario auth refresh`** — a fresh ID token on
|
|
50
|
+
stdout, and a rewrite of all managed token files (tokens live ~1 hour).
|
|
51
|
+
|
|
52
|
+
Agents: see [`skills/cytario-cli.md`](skills/cytario-cli.md) for the
|
|
53
|
+
tool-neutral agent workflow shipped with this repo.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install cytario-cli # or: pip install cytario-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python ≥ 3.10. The CLI is a public OIDC client — no secrets are shipped or
|
|
62
|
+
stored beyond your own refresh grant.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cytario auth login --host https://app.cytario.com
|
|
68
|
+
cytario connections list
|
|
69
|
+
cytario connections setup --all
|
|
70
|
+
aws s3 ls --profile cytario-mybucket
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Host resolution order: `--host`, then `CYTARIO_HOST`, then the last
|
|
74
|
+
signed-in host.
|
|
75
|
+
|
|
76
|
+
## Security model
|
|
77
|
+
|
|
78
|
+
- Your authorization is exactly your Cytario grant on each connection
|
|
79
|
+
(`read-only` / `annotate` / `read-write` / `admin`); bucket sharing stays
|
|
80
|
+
confined to the web app.
|
|
81
|
+
- No long-lived AWS keys are written; the AWS CLI federates per operation
|
|
82
|
+
from the token file, and the CLI refreshes tokens before expiry.
|
|
83
|
+
- Revocation: revoking the CLI session in the identity service (or removing
|
|
84
|
+
your group membership) ends the CLI's access within one token lifetime.
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv sync
|
|
90
|
+
uv run ruff format --check && uv run ruff check
|
|
91
|
+
uv run pytest
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Releases are cut by [python-semantic-release](https://python-semantic-release.readthedocs.io/)
|
|
95
|
+
on Conventional Commits and published to PyPI via trusted publishing.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# cytario-cli
|
|
2
|
+
|
|
3
|
+
CLI for working with [Cytario](https://github.com/cytario) storage connections
|
|
4
|
+
as the signed-in user — designed for AI agents and analysts who need to work
|
|
5
|
+
with connection data using the workstation's standard AWS tooling.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
- **`cytario auth login --host <url>`** — browser sign-in (OAuth 2.0
|
|
10
|
+
Authorization Code + PKCE against the deployment's identity service; a
|
|
11
|
+
loopback redirect receives the result). The refresh grant is stored
|
|
12
|
+
user-private (`~/.config/cytario/cli`, `0600`).
|
|
13
|
+
- **`cytario connections list [--json]`** — the connections visible to you,
|
|
14
|
+
with the storage role and access level your grant resolves to.
|
|
15
|
+
- **`cytario connections setup [--all | <name>]`** — writes one AWS CLI profile
|
|
16
|
+
per connection into `~/.aws/config` (`[profile cytario-<name>]`) with
|
|
17
|
+
`web_identity_token_file`, so `aws` / boto3 / pandas perform
|
|
18
|
+
`AssumeRoleWithWebIdentity` themselves — with exactly your grant's
|
|
19
|
+
authorization, never wider.
|
|
20
|
+
- **`cytario auth token` / `cytario auth refresh`** — a fresh ID token on
|
|
21
|
+
stdout, and a rewrite of all managed token files (tokens live ~1 hour).
|
|
22
|
+
|
|
23
|
+
Agents: see [`skills/cytario-cli.md`](skills/cytario-cli.md) for the
|
|
24
|
+
tool-neutral agent workflow shipped with this repo.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv tool install cytario-cli # or: pip install cytario-cli
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Python ≥ 3.10. The CLI is a public OIDC client — no secrets are shipped or
|
|
33
|
+
stored beyond your own refresh grant.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cytario auth login --host https://app.cytario.com
|
|
39
|
+
cytario connections list
|
|
40
|
+
cytario connections setup --all
|
|
41
|
+
aws s3 ls --profile cytario-mybucket
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Host resolution order: `--host`, then `CYTARIO_HOST`, then the last
|
|
45
|
+
signed-in host.
|
|
46
|
+
|
|
47
|
+
## Security model
|
|
48
|
+
|
|
49
|
+
- Your authorization is exactly your Cytario grant on each connection
|
|
50
|
+
(`read-only` / `annotate` / `read-write` / `admin`); bucket sharing stays
|
|
51
|
+
confined to the web app.
|
|
52
|
+
- No long-lived AWS keys are written; the AWS CLI federates per operation
|
|
53
|
+
from the token file, and the CLI refreshes tokens before expiry.
|
|
54
|
+
- Revocation: revoking the CLI session in the identity service (or removing
|
|
55
|
+
your group membership) ends the CLI's access within one token lifetime.
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv sync
|
|
61
|
+
uv run ruff format --check && uv run ruff check
|
|
62
|
+
uv run pytest
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Releases are cut by [python-semantic-release](https://python-semantic-release.readthedocs.io/)
|
|
66
|
+
on Conventional Commits and published to PyPI via trusted publishing.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[tool.hatch.build.targets.wheel]
|
|
6
|
+
packages = ["src/cytario_cli"]
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "cytario-cli"
|
|
10
|
+
version = "1.0.0"
|
|
11
|
+
description = "CLI for working with Cytario storage connections as the signed-in user (browser OIDC sign-in, AWS CLI profile setup, token refresh)"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
authors = [
|
|
17
|
+
{ name = "Cytario" },
|
|
18
|
+
]
|
|
19
|
+
keywords = [
|
|
20
|
+
"cytario",
|
|
21
|
+
"oidc",
|
|
22
|
+
"aws",
|
|
23
|
+
"sts",
|
|
24
|
+
"web-identity",
|
|
25
|
+
"ai-agent",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Intended Audience :: Science/Research",
|
|
31
|
+
"License :: OSI Approved :: MIT License",
|
|
32
|
+
"Operating System :: OS Independent",
|
|
33
|
+
"Programming Language :: Python :: 3",
|
|
34
|
+
"Programming Language :: Python :: 3.10",
|
|
35
|
+
"Programming Language :: Python :: 3.11",
|
|
36
|
+
"Programming Language :: Python :: 3.12",
|
|
37
|
+
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
39
|
+
"Topic :: Utilities",
|
|
40
|
+
]
|
|
41
|
+
dependencies = [
|
|
42
|
+
"httpx>=0.27",
|
|
43
|
+
"typer>=0.12",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/cytario/cytario-cli"
|
|
48
|
+
Repository = "https://github.com/cytario/cytario-cli"
|
|
49
|
+
Issues = "https://github.com/cytario/cytario-cli/issues"
|
|
50
|
+
Changelog = "https://github.com/cytario/cytario-cli/releases"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
cytario = "cytario_cli.cli:app"
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
dev = [
|
|
57
|
+
"pytest>=8",
|
|
58
|
+
"respx>=0.21",
|
|
59
|
+
"ruff>=0.6",
|
|
60
|
+
"python-semantic-release>=10",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
# python-semantic-release — same no-commit mode as cytario-app-sdk: the
|
|
64
|
+
# version step stamps the working tree, pushes only the tag, and the
|
|
65
|
+
# GitHub Release carries the notes; PyPI publishing is trusted-publishing
|
|
66
|
+
# (OIDC) in the separate publish job.
|
|
67
|
+
[tool.semantic_release]
|
|
68
|
+
commit_parser = "conventional"
|
|
69
|
+
version_variables = ["src/cytario_cli/__init__.py:__version__"]
|
|
70
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
71
|
+
tag_format = "v{version}"
|
|
72
|
+
build_command = "uv build"
|
|
73
|
+
commit_author = "github-actions <actions@users.noreply.github.com>"
|
|
74
|
+
commit_message = "chore(release): {version}\n\nAutomatically generated by python-semantic-release."
|
|
75
|
+
publish_to_pypi = false
|
|
76
|
+
upload_to_vcs_release = true
|
|
77
|
+
|
|
78
|
+
[tool.semantic_release.branches.main]
|
|
79
|
+
match = "main"
|
|
80
|
+
|
|
81
|
+
[tool.semantic_release.commit_parser_options]
|
|
82
|
+
minor_tags = ["feat"]
|
|
83
|
+
patch_tags = ["fix", "perf"]
|
|
84
|
+
parse_squash_commits = true
|
|
85
|
+
ignore_merge_commits = true
|
|
86
|
+
|
|
87
|
+
[tool.ruff]
|
|
88
|
+
line-length = 110
|
|
89
|
+
target-version = "py310"
|
|
90
|
+
src = ["src", "tests"]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint]
|
|
93
|
+
select = ["ALL"]
|
|
94
|
+
ignore = [
|
|
95
|
+
"COM812", # trailing-comma rule conflicts with the formatter
|
|
96
|
+
"ISC001", # implicit-string-concat conflicts with the formatter
|
|
97
|
+
"D203", # one-blank-line-before-class (conflicts with D211)
|
|
98
|
+
"D213", # multi-line-summary-second-line (conflicts with D212)
|
|
99
|
+
"FBT001", # boolean-typed positional/keyword args are idiomatic Typer flags
|
|
100
|
+
"FBT002",
|
|
101
|
+
"FBT003",
|
|
102
|
+
"CPY001", # no copyright-notice convention in sibling Cytario repos
|
|
103
|
+
"TRY003", # long messages in raise are the actionable error text here
|
|
104
|
+
"EM101", # string-literal raise messages are fine for this size of CLI
|
|
105
|
+
"EM102", # f-string raise messages likewise
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
[tool.ruff.lint.per-file-ignores]
|
|
109
|
+
"tests/*" = [
|
|
110
|
+
"S101", # asserts are the point of a test suite
|
|
111
|
+
"S105", # "hardcoded password" strings are test fixtures
|
|
112
|
+
"S106", # same, as a function argument
|
|
113
|
+
"S107", # same, as a function default arg
|
|
114
|
+
"S108", # /tmp paths in profile-writer tests are placeholder paths
|
|
115
|
+
"D", # docstrings not required on every test function
|
|
116
|
+
"ANN", # type annotations not required in tests
|
|
117
|
+
"ARG001", # side-effecting fixtures are requested but not read
|
|
118
|
+
"PLC0415", # importing the module under monkeypatch inside the test is the point
|
|
119
|
+
"PLR2004", # magic values are fine in test assertions
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
[tool.pytest.ini_options]
|
|
123
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cytario-cli
|
|
3
|
+
description: This skill should be used when the user asks to "work with cytario data", "access cytario storage", "list my cytario connections", "download or upload files to a cytario bucket", "read analysis results from cytario", "use the cytario CLI", "set up AWS profiles for cytario", or when a task involves S3 data managed by Cytario (buckets, connections, imaging datasets, annotation sidecars, analysis outputs) that must be accessed programmatically as the signed-in user.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Cytario CLI — agent workflow for cytario-managed data
|
|
7
|
+
|
|
8
|
+
The `cytario` CLI (Python, `pip install cytario-cli` or `uv tool install cytario-cli`)
|
|
9
|
+
lets you work with the data of the user's Cytario storage connections using the
|
|
10
|
+
workstation's standard AWS tooling — never with the user's browser credentials.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. **Sign in (once per session):** run `cytario auth login --host <cytario-host>`
|
|
15
|
+
(the host is remembered; `CYTARIO_HOST` also works). A browser window opens for
|
|
16
|
+
the user to sign in; the CLI receives the result on a loopback redirect and
|
|
17
|
+
stores a refresh grant in `~/.config/cytario/cli/`. If the user has recently
|
|
18
|
+
signed in on this machine, check first with `cytario auth status`.
|
|
19
|
+
2. **Discover the data:** `cytario connections list --json` returns every
|
|
20
|
+
connection visible to the user with bucket, prefix, region, endpoints, and the
|
|
21
|
+
user's access level (`read-only`, `annotate`, `read-write`, `admin`).
|
|
22
|
+
3. **Configure AWS profiles:** `cytario connections setup --all` (or with a
|
|
23
|
+
connection name) writes one AWS CLI profile per connection
|
|
24
|
+
(`~/.aws/config`, `[profile cytario-<name>]`) plus token files under
|
|
25
|
+
`~/.aws/cytario/`. Standard tooling then does the federation itself.
|
|
26
|
+
4. **Work with the data** via the generated profiles:
|
|
27
|
+
- `aws --profile cytario-<name> s3 ls s3://<bucket>/<prefix>`
|
|
28
|
+
- `aws --profile cytario-<name> s3 cp ...`
|
|
29
|
+
- boto3: `boto3.session.Session(profile_name="cytario-<name>")`
|
|
30
|
+
- pandas/pyarrow: read Parquet/CSV after `s3 cp` to a scratch dir.
|
|
31
|
+
5. **Keep tokens fresh during long work:** ID tokens live ~1 hour. The AWS CLI
|
|
32
|
+
re-reads the token file on every `AssumeRoleWithWebIdentity`, so before any
|
|
33
|
+
operation expected to outlast a token (or on `ExpiredToken` errors) run
|
|
34
|
+
`cytario auth refresh` to rewrite all token files from a fresh ID token.
|
|
35
|
+
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
- **You act as the user, with exactly their grant.** A `read-only` connection
|
|
39
|
+
must not be written to; `annotate` permits annotation sidecar writes only;
|
|
40
|
+
bucket *sharing* (`s3:PutBucketPolicy`) is always denied outside the web app.
|
|
41
|
+
Never try to widen access — surface the access level instead.
|
|
42
|
+
- **Never handle the user's browser session, password, or the refresh grant
|
|
43
|
+
file** (`~/.config/cytario/cli/state.json`). The CLI owns them.
|
|
44
|
+
- **Never write long-lived AWS access keys.** The profiles use
|
|
45
|
+
`web_identity_token_file`; the credential mint happens per operation.
|
|
46
|
+
- Prefer `connections list --json` (machine-readable) over parsing table output.
|
|
47
|
+
- Name-locality: profiles are `cytario-<connection-name>`; token files
|
|
48
|
+
`~/.aws/cytario/<connection-name>/id_token`.
|
|
49
|
+
- If `auth token`/`connections list` returns "rejected", the grant was revoked
|
|
50
|
+
or expired — ask the user to sign in again; do not retry in a loop.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Client for cytario-web's /api/me/connections endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .awsconfig import Connection
|
|
8
|
+
|
|
9
|
+
HTTP_UNAUTHORIZED = 401
|
|
10
|
+
HTTP_OK = 200
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(Exception):
|
|
14
|
+
"""Raised when the my-connections endpoint fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def list_connections(host: str, id_token: str) -> list[Connection]:
|
|
18
|
+
"""Fetch the signed-in user's visible connections with their resolved grants."""
|
|
19
|
+
url = f"{host.rstrip('/')}/api/me/connections"
|
|
20
|
+
response = httpx.get(url, headers={"Authorization": f"Bearer {id_token}"}, timeout=15)
|
|
21
|
+
if response.status_code == HTTP_UNAUTHORIZED:
|
|
22
|
+
raise ApiError("The token was rejected. Run `cytario auth login` and try again.")
|
|
23
|
+
if response.status_code != HTTP_OK:
|
|
24
|
+
raise ApiError(f"GET {url} failed ({response.status_code}): {response.text}")
|
|
25
|
+
payload = response.json()
|
|
26
|
+
return [Connection.from_api(row) for row in payload.get("connections", [])]
|