tarnished-cli 0.1.2__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.
- tarnished_cli-0.1.2/.gitignore +180 -0
- tarnished_cli-0.1.2/PKG-INFO +91 -0
- tarnished_cli-0.1.2/README.md +79 -0
- tarnished_cli-0.1.2/pyproject.toml +71 -0
- tarnished_cli-0.1.2/src/tarnished_cli/__init__.py +1 -0
- tarnished_cli-0.1.2/src/tarnished_cli/auth_storage.py +135 -0
- tarnished_cli-0.1.2/src/tarnished_cli/client.py +313 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/__init__.py +1 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/admin.py +182 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/analytics.py +117 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/applications.py +381 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/auth.py +152 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/dashboard.py +37 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/exports.py +48 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/imports.py +78 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/job_leads.py +131 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/preferences.py +45 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/profile.py +35 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/round_types.py +77 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/rounds.py +232 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/statuses.py +73 -0
- tarnished_cli-0.1.2/src/tarnished_cli/commands/user_settings.py +42 -0
- tarnished_cli-0.1.2/src/tarnished_cli/config.py +93 -0
- tarnished_cli-0.1.2/src/tarnished_cli/input.py +34 -0
- tarnished_cli-0.1.2/src/tarnished_cli/main.py +79 -0
- tarnished_cli-0.1.2/src/tarnished_cli/models/__init__.py +39 -0
- tarnished_cli-0.1.2/src/tarnished_cli/models/requests.py +254 -0
- tarnished_cli-0.1.2/src/tarnished_cli/output.py +58 -0
- tarnished_cli-0.1.2/src/tarnished_cli/state.py +95 -0
- tarnished_cli-0.1.2/tests/conftest.py +15 -0
- tarnished_cli-0.1.2/tests/test_admin_commands.py +94 -0
- tarnished_cli-0.1.2/tests/test_analytics_commands.py +77 -0
- tarnished_cli-0.1.2/tests/test_applications_commands.py +227 -0
- tarnished_cli-0.1.2/tests/test_auth_commands.py +220 -0
- tarnished_cli-0.1.2/tests/test_auth_storage.py +56 -0
- tarnished_cli-0.1.2/tests/test_cli_config.py +9 -0
- tarnished_cli-0.1.2/tests/test_client.py +105 -0
- tarnished_cli-0.1.2/tests/test_dashboard_commands.py +26 -0
- tarnished_cli-0.1.2/tests/test_export_commands.py +23 -0
- tarnished_cli-0.1.2/tests/test_import_commands.py +85 -0
- tarnished_cli-0.1.2/tests/test_job_leads_commands.py +79 -0
- tarnished_cli-0.1.2/tests/test_main.py +30 -0
- tarnished_cli-0.1.2/tests/test_output_contract.py +17 -0
- tarnished_cli-0.1.2/tests/test_profile_commands.py +36 -0
- tarnished_cli-0.1.2/tests/test_round_types_commands.py +28 -0
- tarnished_cli-0.1.2/tests/test_rounds_commands.py +124 -0
- tarnished_cli-0.1.2/tests/test_statuses_commands.py +62 -0
- tarnished_cli-0.1.2/tests/test_user_settings_commands.py +53 -0
- tarnished_cli-0.1.2/uv.lock +614 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
node_modules/
|
|
5
|
+
.env
|
|
6
|
+
data/
|
|
7
|
+
uploads/
|
|
8
|
+
*.db
|
|
9
|
+
.uv/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
.worktrees/
|
|
16
|
+
mise.toml
|
|
17
|
+
docs/
|
|
18
|
+
scripts/
|
|
19
|
+
.claude/
|
|
20
|
+
.devcontainer/
|
|
21
|
+
.planning/
|
|
22
|
+
.serena/
|
|
23
|
+
CLAUDE.md
|
|
24
|
+
|
|
25
|
+
.firecrawl/
|
|
26
|
+
|
|
27
|
+
# Ralph Loop (autonomous implementation)
|
|
28
|
+
ralph.yml
|
|
29
|
+
ralph-loop.sh
|
|
30
|
+
PROMPT.md
|
|
31
|
+
specs/
|
|
32
|
+
.ralph/
|
|
33
|
+
.claude/handoff*.md
|
|
34
|
+
|
|
35
|
+
# Design guidelines - development notes, not source code
|
|
36
|
+
frontend/DESIGN_GUIDELINES.md
|
|
37
|
+
backend/DESIGN_GUIDELINES.md
|
|
38
|
+
|
|
39
|
+
# Seed scripts - development only
|
|
40
|
+
backend/seed_*.py
|
|
41
|
+
|
|
42
|
+
# Generated requirements.txt
|
|
43
|
+
backend/requirements.txt
|
|
44
|
+
|
|
45
|
+
# Pre-commit framework (using Husky instead)
|
|
46
|
+
.pre-commit-config.yaml
|
|
47
|
+
|
|
48
|
+
# Playwright MCP screenshots and snapshots
|
|
49
|
+
.playwright-mcp/
|
|
50
|
+
|
|
51
|
+
# macOS
|
|
52
|
+
.DS_Store
|
|
53
|
+
.DS_Store?
|
|
54
|
+
._*
|
|
55
|
+
.Spotlight-V100
|
|
56
|
+
.Trashes
|
|
57
|
+
ehthumbs.db
|
|
58
|
+
Thumbs.db
|
|
59
|
+
|
|
60
|
+
# Temporary directories and files
|
|
61
|
+
temp/
|
|
62
|
+
temp2/
|
|
63
|
+
tmp/
|
|
64
|
+
*.tmp
|
|
65
|
+
*~
|
|
66
|
+
|
|
67
|
+
# Core dumps
|
|
68
|
+
/core
|
|
69
|
+
core.*
|
|
70
|
+
|
|
71
|
+
# Editor swap files
|
|
72
|
+
*.swp
|
|
73
|
+
*.swo
|
|
74
|
+
*.swn
|
|
75
|
+
|
|
76
|
+
# Session backup files
|
|
77
|
+
*session-is-being-continued*.txt
|
|
78
|
+
*.bak
|
|
79
|
+
|
|
80
|
+
# ============================================================================
|
|
81
|
+
# Frontend (Node.js/Yarn/Vite)
|
|
82
|
+
# ============================================================================
|
|
83
|
+
|
|
84
|
+
# Yarn
|
|
85
|
+
.yarn/
|
|
86
|
+
.pnp.*
|
|
87
|
+
|
|
88
|
+
# Vite
|
|
89
|
+
.vite/
|
|
90
|
+
|
|
91
|
+
# Environment variants
|
|
92
|
+
.env.local
|
|
93
|
+
.env.*.local
|
|
94
|
+
|
|
95
|
+
# Backend environment files
|
|
96
|
+
backend/.env.postgresql
|
|
97
|
+
backend/.env.*
|
|
98
|
+
|
|
99
|
+
# TypeScript
|
|
100
|
+
*.tsbuildinfo
|
|
101
|
+
|
|
102
|
+
# ESLint
|
|
103
|
+
.eslintcache
|
|
104
|
+
|
|
105
|
+
# Test coverage
|
|
106
|
+
coverage/
|
|
107
|
+
*.lcov
|
|
108
|
+
|
|
109
|
+
# Logs
|
|
110
|
+
*.log
|
|
111
|
+
npm-debug.log*
|
|
112
|
+
yarn-debug.log*
|
|
113
|
+
yarn-error.log*
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# Backend (Python/Testing)
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
# Python artifacts
|
|
120
|
+
*.pyo
|
|
121
|
+
*.so
|
|
122
|
+
*.py[cod]
|
|
123
|
+
*.egg
|
|
124
|
+
|
|
125
|
+
# Database files
|
|
126
|
+
*.sqlite
|
|
127
|
+
*.sqlite3
|
|
128
|
+
|
|
129
|
+
# IDE configs
|
|
130
|
+
.vscode/
|
|
131
|
+
.idea/
|
|
132
|
+
|
|
133
|
+
# Test coverage
|
|
134
|
+
.coverage
|
|
135
|
+
htmlcov/
|
|
136
|
+
*.cover
|
|
137
|
+
.pytest_cache/
|
|
138
|
+
.hypothesis/
|
|
139
|
+
.tox/
|
|
140
|
+
|
|
141
|
+
# MyPy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Alembic cache (not migration files)
|
|
147
|
+
alembic/versions/__pycache__/
|
|
148
|
+
|
|
149
|
+
# ============================================================================
|
|
150
|
+
# Project Level (Docker/Cache)
|
|
151
|
+
# ============================================================================
|
|
152
|
+
|
|
153
|
+
# Docker overrides
|
|
154
|
+
docker-compose.override.yml
|
|
155
|
+
docker-compose.override.yaml
|
|
156
|
+
|
|
157
|
+
# Docker build artifacts
|
|
158
|
+
.docker/
|
|
159
|
+
|
|
160
|
+
# CI/CD local files
|
|
161
|
+
.github/workflows/*.local.yml
|
|
162
|
+
.github/workflows/*.local.yaml
|
|
163
|
+
|
|
164
|
+
# Cache directories
|
|
165
|
+
.cache/
|
|
166
|
+
.npm/
|
|
167
|
+
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# Browser Extension
|
|
170
|
+
# ============================================================================
|
|
171
|
+
|
|
172
|
+
# Extension build artifacts
|
|
173
|
+
*.xpi
|
|
174
|
+
|
|
175
|
+
# Extension test/dev files
|
|
176
|
+
extension/test-form.html
|
|
177
|
+
|
|
178
|
+
# Don't mix npm and yarn in extension
|
|
179
|
+
extension/package-lock.json
|
|
180
|
+
.playwright/
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tarnished-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Agent-first CLI for Tarnished
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: email-validator>=2.0.0
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: keyring>=25.7.0
|
|
9
|
+
Requires-Dist: pydantic>=2.12.0
|
|
10
|
+
Requires-Dist: typer>=0.23.1
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Tarnished CLI
|
|
14
|
+
|
|
15
|
+
Agent-first command-line interface for Tarnished.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Preferred install path:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install tarnished-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Alternative:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pipx install tarnished-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Install from a built wheel before PyPI publication:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install ./dist/tarnished_cli-0.1.2-py3-none-any.whl
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## OpenClaw / Agent Install
|
|
38
|
+
|
|
39
|
+
Recommended:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv tool install tarnished-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Version-pinned:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv tool install 'tarnished-cli==0.1.2'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then run:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
tarnished --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd cli
|
|
61
|
+
uv sync
|
|
62
|
+
uv run tarnished --help
|
|
63
|
+
uv run pytest -q
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Release
|
|
67
|
+
|
|
68
|
+
The repository release workflow builds CLI distributions and uploads them to the GitHub release.
|
|
69
|
+
|
|
70
|
+
PyPI publication is optional and is controlled by the `publish_cli_package` workflow input.
|
|
71
|
+
|
|
72
|
+
### One-Time PyPI Trusted Publishing Setup
|
|
73
|
+
|
|
74
|
+
1. Create the `tarnished-cli` project on PyPI.
|
|
75
|
+
2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
|
|
76
|
+
3. Use these values:
|
|
77
|
+
- Owner: `markoonakic`
|
|
78
|
+
- Repository: `tarnished`
|
|
79
|
+
- Workflow name: `release.yml`
|
|
80
|
+
- Environment name: `pypi`
|
|
81
|
+
4. After that, run the GitHub release workflow with:
|
|
82
|
+
- `publish_cli_package=true`
|
|
83
|
+
|
|
84
|
+
### Release Outputs
|
|
85
|
+
|
|
86
|
+
The release workflow publishes:
|
|
87
|
+
|
|
88
|
+
- `cli/dist/*.whl`
|
|
89
|
+
- `cli/dist/*.tar.gz`
|
|
90
|
+
|
|
91
|
+
to the GitHub release, and optionally to PyPI.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Tarnished CLI
|
|
2
|
+
|
|
3
|
+
Agent-first command-line interface for Tarnished.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
Preferred install path:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv tool install tarnished-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Alternative:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install tarnished-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Install from a built wheel before PyPI publication:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install ./dist/tarnished_cli-0.1.2-py3-none-any.whl
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## OpenClaw / Agent Install
|
|
26
|
+
|
|
27
|
+
Recommended:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv tool install tarnished-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Version-pinned:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install 'tarnished-cli==0.1.2'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
tarnished --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
cd cli
|
|
49
|
+
uv sync
|
|
50
|
+
uv run tarnished --help
|
|
51
|
+
uv run pytest -q
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Release
|
|
55
|
+
|
|
56
|
+
The repository release workflow builds CLI distributions and uploads them to the GitHub release.
|
|
57
|
+
|
|
58
|
+
PyPI publication is optional and is controlled by the `publish_cli_package` workflow input.
|
|
59
|
+
|
|
60
|
+
### One-Time PyPI Trusted Publishing Setup
|
|
61
|
+
|
|
62
|
+
1. Create the `tarnished-cli` project on PyPI.
|
|
63
|
+
2. In the PyPI project settings, add a Trusted Publisher for this GitHub repository.
|
|
64
|
+
3. Use these values:
|
|
65
|
+
- Owner: `markoonakic`
|
|
66
|
+
- Repository: `tarnished`
|
|
67
|
+
- Workflow name: `release.yml`
|
|
68
|
+
- Environment name: `pypi`
|
|
69
|
+
4. After that, run the GitHub release workflow with:
|
|
70
|
+
- `publish_cli_package=true`
|
|
71
|
+
|
|
72
|
+
### Release Outputs
|
|
73
|
+
|
|
74
|
+
The release workflow publishes:
|
|
75
|
+
|
|
76
|
+
- `cli/dist/*.whl`
|
|
77
|
+
- `cli/dist/*.tar.gz`
|
|
78
|
+
|
|
79
|
+
to the GitHub release, and optionally to PyPI.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tarnished-cli"
|
|
3
|
+
version = "0.1.2"
|
|
4
|
+
description = "Agent-first CLI for Tarnished"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28.1",
|
|
9
|
+
"keyring>=25.7.0",
|
|
10
|
+
"pydantic>=2.12.0",
|
|
11
|
+
"email-validator>=2.0.0",
|
|
12
|
+
"typer>=0.23.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
tarnished = "tarnished_cli.main:app"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/tarnished_cli"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pyright>=1.1.408",
|
|
31
|
+
"pytest>=9.0.2",
|
|
32
|
+
"ruff>=0.15.1",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
target-version = "py312"
|
|
37
|
+
line-length = 88
|
|
38
|
+
src = ["src", "tests"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = [
|
|
42
|
+
"E",
|
|
43
|
+
"W",
|
|
44
|
+
"F",
|
|
45
|
+
"I",
|
|
46
|
+
"B",
|
|
47
|
+
"C4",
|
|
48
|
+
"UP",
|
|
49
|
+
"SIM",
|
|
50
|
+
]
|
|
51
|
+
ignore = [
|
|
52
|
+
"E501",
|
|
53
|
+
"B008",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint.per-file-ignores]
|
|
57
|
+
"tests/**/*.py" = [
|
|
58
|
+
"B017",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.format]
|
|
62
|
+
quote-style = "double"
|
|
63
|
+
indent-style = "space"
|
|
64
|
+
|
|
65
|
+
[tool.pyright]
|
|
66
|
+
pythonVersion = "3.12"
|
|
67
|
+
typeCheckingMode = "basic"
|
|
68
|
+
reportMissingImports = true
|
|
69
|
+
reportMissingTypeStubs = false
|
|
70
|
+
venvPath = "."
|
|
71
|
+
venv = ".venv"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tarnished CLI package."""
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import keyring
|
|
9
|
+
from keyring.errors import KeyringError, PasswordDeleteError
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from tarnished_cli.config import resolve_config_dir
|
|
13
|
+
|
|
14
|
+
ACCESS_TOKEN_ENV = "TARNISHED_ACCESS_TOKEN"
|
|
15
|
+
REFRESH_TOKEN_ENV = "TARNISHED_REFRESH_TOKEN"
|
|
16
|
+
API_KEY_ENV = "TARNISHED_API_KEY"
|
|
17
|
+
KEYRING_SERVICE = "tarnished-cli"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class StoredAuth(BaseModel):
|
|
21
|
+
access_token: str | None = None
|
|
22
|
+
refresh_token: str | None = None
|
|
23
|
+
api_key: str | None = None
|
|
24
|
+
|
|
25
|
+
def is_empty(self) -> bool:
|
|
26
|
+
return not any([self.access_token, self.refresh_token, self.api_key])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_auth_path(profile: str = "default", config_dir: Path | None = None) -> Path:
|
|
30
|
+
return resolve_config_dir(config_dir) / f"auth-{profile}.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _keyring_account(profile: str = "default", config_dir: Path | None = None) -> str:
|
|
34
|
+
resolved_dir = resolve_config_dir(config_dir)
|
|
35
|
+
return f"{resolved_dir}:{profile}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _env_auth() -> StoredAuth | None:
|
|
39
|
+
access_token = os.getenv(ACCESS_TOKEN_ENV)
|
|
40
|
+
refresh_token = os.getenv(REFRESH_TOKEN_ENV)
|
|
41
|
+
api_key = os.getenv(API_KEY_ENV)
|
|
42
|
+
if not any([access_token, refresh_token, api_key]):
|
|
43
|
+
return None
|
|
44
|
+
return StoredAuth(
|
|
45
|
+
access_token=access_token,
|
|
46
|
+
refresh_token=refresh_token,
|
|
47
|
+
api_key=api_key,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _read_auth_file(path: Path) -> StoredAuth:
|
|
52
|
+
if not path.exists():
|
|
53
|
+
return StoredAuth()
|
|
54
|
+
return StoredAuth.model_validate(json.loads(path.read_text()))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _write_auth_file(path: Path, auth: StoredAuth) -> None:
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
with contextlib.suppress(OSError):
|
|
60
|
+
path.parent.chmod(0o700)
|
|
61
|
+
path.write_text(json.dumps(auth.model_dump(mode="json"), sort_keys=True) + "\n")
|
|
62
|
+
with contextlib.suppress(OSError):
|
|
63
|
+
path.chmod(0o600)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _remove_auth_file(path: Path) -> None:
|
|
67
|
+
if path.exists():
|
|
68
|
+
path.unlink()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_auth(
|
|
72
|
+
profile: str = "default",
|
|
73
|
+
*,
|
|
74
|
+
config_dir: Path | None = None,
|
|
75
|
+
prefer_keyring: bool = True,
|
|
76
|
+
) -> StoredAuth:
|
|
77
|
+
env_auth = _env_auth()
|
|
78
|
+
if env_auth is not None:
|
|
79
|
+
return env_auth
|
|
80
|
+
|
|
81
|
+
if prefer_keyring:
|
|
82
|
+
account = _keyring_account(profile, config_dir)
|
|
83
|
+
try:
|
|
84
|
+
payload = keyring.get_password(KEYRING_SERVICE, account)
|
|
85
|
+
except (KeyringError, RuntimeError):
|
|
86
|
+
payload = None
|
|
87
|
+
if payload:
|
|
88
|
+
return StoredAuth.model_validate_json(payload)
|
|
89
|
+
|
|
90
|
+
return _read_auth_file(resolve_auth_path(profile, config_dir))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def save_auth(
|
|
94
|
+
auth: StoredAuth,
|
|
95
|
+
profile: str = "default",
|
|
96
|
+
*,
|
|
97
|
+
config_dir: Path | None = None,
|
|
98
|
+
prefer_keyring: bool = True,
|
|
99
|
+
) -> Path:
|
|
100
|
+
path = resolve_auth_path(profile, config_dir)
|
|
101
|
+
|
|
102
|
+
if prefer_keyring:
|
|
103
|
+
account = _keyring_account(profile, config_dir)
|
|
104
|
+
try:
|
|
105
|
+
if auth.is_empty():
|
|
106
|
+
with contextlib.suppress(PasswordDeleteError):
|
|
107
|
+
keyring.delete_password(KEYRING_SERVICE, account)
|
|
108
|
+
else:
|
|
109
|
+
keyring.set_password(
|
|
110
|
+
KEYRING_SERVICE, account, auth.model_dump_json(exclude_none=False)
|
|
111
|
+
)
|
|
112
|
+
_remove_auth_file(path)
|
|
113
|
+
return path
|
|
114
|
+
except (KeyringError, RuntimeError):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
_write_auth_file(path, auth)
|
|
118
|
+
return path
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clear_auth(
|
|
122
|
+
profile: str = "default",
|
|
123
|
+
*,
|
|
124
|
+
config_dir: Path | None = None,
|
|
125
|
+
prefer_keyring: bool = True,
|
|
126
|
+
) -> None:
|
|
127
|
+
path = resolve_auth_path(profile, config_dir)
|
|
128
|
+
if prefer_keyring:
|
|
129
|
+
account = _keyring_account(profile, config_dir)
|
|
130
|
+
try:
|
|
131
|
+
with contextlib.suppress(PasswordDeleteError):
|
|
132
|
+
keyring.delete_password(KEYRING_SERVICE, account)
|
|
133
|
+
except (KeyringError, RuntimeError):
|
|
134
|
+
pass
|
|
135
|
+
_remove_auth_file(path)
|