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.
@@ -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,7 @@
1
+ """Cytario CLI — work with Cytario storage connections as the signed-in user."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.0.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """Command entry point: `python -m cytario_cli`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import app
6
+
7
+ if __name__ == "__main__":
8
+ app()
@@ -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", [])]