aicademy 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.
- aicademy-0.1.0/.github/workflows/publish.yml +38 -0
- aicademy-0.1.0/.gitignore +81 -0
- aicademy-0.1.0/PKG-INFO +185 -0
- aicademy-0.1.0/README.md +155 -0
- aicademy-0.1.0/aicademy_cli/__init__.py +1 -0
- aicademy-0.1.0/aicademy_cli/api.py +106 -0
- aicademy-0.1.0/aicademy_cli/commands/__init__.py +1 -0
- aicademy-0.1.0/aicademy_cli/commands/auth.py +102 -0
- aicademy-0.1.0/aicademy_cli/commands/question.py +185 -0
- aicademy-0.1.0/aicademy_cli/commands/tools.py +157 -0
- aicademy-0.1.0/aicademy_cli/commands/verify.py +99 -0
- aicademy-0.1.0/aicademy_cli/config.py +54 -0
- aicademy-0.1.0/aicademy_cli/core/__init__.py +1 -0
- aicademy-0.1.0/aicademy_cli/core/kind.py +25 -0
- aicademy-0.1.0/aicademy_cli/core/utils.py +88 -0
- aicademy-0.1.0/aicademy_cli/main.py +100 -0
- aicademy-0.1.0/pyproject.toml +69 -0
- aicademy-0.1.0/uv.lock +688 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
# Optionally allow manual triggers
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi-publish:
|
|
11
|
+
name: Build and publish Python package
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
# Specifying a GitHub environment is strongly encouraged for Trusted Publishing
|
|
15
|
+
environment: pypi
|
|
16
|
+
|
|
17
|
+
permissions:
|
|
18
|
+
# IMPORTANT: this permission is mandatory for trusted publishing
|
|
19
|
+
id-token: write
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.12"
|
|
30
|
+
|
|
31
|
+
- name: Install build tool
|
|
32
|
+
run: python -m pip install --upgrade build
|
|
33
|
+
|
|
34
|
+
- name: Build a binary wheel and a source tarball
|
|
35
|
+
run: python -m build
|
|
36
|
+
|
|
37
|
+
- name: Publish package to PyPI
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
*.manifest
|
|
31
|
+
*.spec
|
|
32
|
+
|
|
33
|
+
# Installer logs
|
|
34
|
+
pip-log.txt
|
|
35
|
+
pip-delete-this-directory.txt
|
|
36
|
+
|
|
37
|
+
# Unit test / coverage reports
|
|
38
|
+
htmlcov/
|
|
39
|
+
.tox/
|
|
40
|
+
.nox/
|
|
41
|
+
.coverage
|
|
42
|
+
.coverage.*
|
|
43
|
+
.cache
|
|
44
|
+
nosetests.xml
|
|
45
|
+
coverage.xml
|
|
46
|
+
*.cover
|
|
47
|
+
*.py,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
cover/
|
|
51
|
+
|
|
52
|
+
# Environments
|
|
53
|
+
.env
|
|
54
|
+
.venv
|
|
55
|
+
env/
|
|
56
|
+
venv/
|
|
57
|
+
ENV/
|
|
58
|
+
env.bak/
|
|
59
|
+
venv.bak/
|
|
60
|
+
|
|
61
|
+
# Secrets & Local Config
|
|
62
|
+
.env
|
|
63
|
+
.env.local
|
|
64
|
+
.env.*
|
|
65
|
+
*.pem
|
|
66
|
+
*.key
|
|
67
|
+
*.db
|
|
68
|
+
*.sqlite
|
|
69
|
+
*.sqlite3
|
|
70
|
+
|
|
71
|
+
# IDEs / Editors / OS
|
|
72
|
+
.vscode/
|
|
73
|
+
.idea/
|
|
74
|
+
*.swp
|
|
75
|
+
*.swo
|
|
76
|
+
*~
|
|
77
|
+
.DS_Store
|
|
78
|
+
|
|
79
|
+
# Typer / Hatch / uv cache
|
|
80
|
+
.hatch/
|
|
81
|
+
.uv/
|
aicademy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aicademy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Aicademy Practice CLI — solve Kubernetes exam scenarios locally with KIND
|
|
5
|
+
Project-URL: Homepage, https://aicademy.ac/practice
|
|
6
|
+
Project-URL: Repository, https://github.com/devcrypted/aicademy-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/devcrypted/aicademy-cli/issues
|
|
8
|
+
Author-email: Aicademy <hello@aicademy.ac>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: cka,ckad,cks,cli,devops,kubernetes,practice
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Education
|
|
23
|
+
Classifier: Topic :: System :: Systems Administration
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: httpx>=0.27.0
|
|
26
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
27
|
+
Requires-Dist: rich>=13.0.0
|
|
28
|
+
Requires-Dist: typer[all]>=0.12.0
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Aicademy CLI
|
|
32
|
+
|
|
33
|
+
> Practice CKA, CKAD, and CKS exam scenarios locally — powered by KIND + Aicademy API.
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/aicademy/)
|
|
36
|
+
[](https://pypi.org/project/aicademy/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
### Using pip
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install aicademy
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Using uv (recommended)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install aicademy
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Using pipx
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pipx install aicademy
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 1. Login to Aicademy
|
|
63
|
+
aicademy login
|
|
64
|
+
|
|
65
|
+
# 2. Check / install prerequisites
|
|
66
|
+
aicademy install-tool all --check
|
|
67
|
+
aicademy install-tool all
|
|
68
|
+
|
|
69
|
+
# 3. Start a practice question (creates KIND cluster automatically)
|
|
70
|
+
aicademy question start cka-01
|
|
71
|
+
|
|
72
|
+
# 4. Read the full task instructions in your terminal
|
|
73
|
+
aicademy question instructions
|
|
74
|
+
|
|
75
|
+
# 5. Solve the scenario using kubectl, helm, etc.
|
|
76
|
+
|
|
77
|
+
# 6. Verify your solution
|
|
78
|
+
aicademy verify
|
|
79
|
+
|
|
80
|
+
# 7. Clean up the cluster
|
|
81
|
+
aicademy question clear
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Command Reference
|
|
85
|
+
|
|
86
|
+
| Command | Description |
|
|
87
|
+
| ------------------------------------------- | ------------------------------------------------- |
|
|
88
|
+
| `aicademy login` | Authenticate (browser flow or direct token) |
|
|
89
|
+
| `aicademy logout` | Clear stored credentials |
|
|
90
|
+
| `aicademy auth whoami` | Verify token validity |
|
|
91
|
+
| `aicademy question start <id>` | Start question environment (creates KIND cluster) |
|
|
92
|
+
| `aicademy question instructions [id]` | Show full task instructions in terminal |
|
|
93
|
+
| `aicademy question instructions [id] --web` | Open question page in browser |
|
|
94
|
+
| `aicademy question clear [id]` | Delete KIND cluster and clear session |
|
|
95
|
+
| `aicademy verify [id]` | Run verify.sh and report result |
|
|
96
|
+
| `aicademy install-tool <name>` | Install kubectl / kind / docker / all |
|
|
97
|
+
| `aicademy install-tool <name> --check` | Check if tool is installed (no install) |
|
|
98
|
+
| `aicademy install-tool <name> --dry-run` | Preview install commands |
|
|
99
|
+
|
|
100
|
+
## Prerequisites
|
|
101
|
+
|
|
102
|
+
| Tool | Purpose | Install |
|
|
103
|
+
| ------- | ----------------- | ------------------------------- |
|
|
104
|
+
| Docker | Runs KIND nodes | `aicademy install-tool docker` |
|
|
105
|
+
| kubectl | Kubernetes CLI | `aicademy install-tool kubectl` |
|
|
106
|
+
| kind | Local K8s cluster | `aicademy install-tool kind` |
|
|
107
|
+
|
|
108
|
+
## OS Support
|
|
109
|
+
|
|
110
|
+
| OS | Package Manager |
|
|
111
|
+
| ---------| ------------------------|
|
|
112
|
+
| Windows | winget |
|
|
113
|
+
| macOS | Homebrew |
|
|
114
|
+
| Linux | Official shell scripts |
|
|
115
|
+
|
|
116
|
+
## Categories
|
|
117
|
+
|
|
118
|
+
| Exam | Slug | Questions | Free |
|
|
119
|
+
| --------------------------------------------| --------| -----------| ------|
|
|
120
|
+
| Certified Kubernetes Administrator | `cka` | 20 | 10 |
|
|
121
|
+
| Certified Kubernetes Application Developer | `ckad` | 20 | 10 |
|
|
122
|
+
| Certified Kubernetes Security Specialist | `cks` | 20 | 10 |
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
### Project Structure
|
|
127
|
+
|
|
128
|
+
The codebase is organized modularly:
|
|
129
|
+
|
|
130
|
+
- `aicademy_cli/main.py`: The entry point and top-level Typer application.
|
|
131
|
+
- `aicademy_cli/commands/`: All user-facing Typer CLI groups (`auth`, `question`, `tools`, `verify`).
|
|
132
|
+
- `aicademy_cli/api.py`: Centralized HTTP requests and error handling.
|
|
133
|
+
- `aicademy_cli/core/`: Internal logic like cluster management (`kind.py`) and helper methods (`utils.py`).
|
|
134
|
+
|
|
135
|
+
### Using uv
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Clone and install in dev mode
|
|
139
|
+
git clone https://github.com/devcrypted/aicademy-cli
|
|
140
|
+
cd aicademy-cli
|
|
141
|
+
uv sync
|
|
142
|
+
|
|
143
|
+
# Run against local dev server
|
|
144
|
+
AICADEMY_API_URL=http://localhost:5173 uv run aicademy login
|
|
145
|
+
|
|
146
|
+
# Run tests
|
|
147
|
+
uv run pytest
|
|
148
|
+
|
|
149
|
+
# Lint
|
|
150
|
+
uv run ruff check .
|
|
151
|
+
uv run mypy aicademy_cli/
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Building the wheel
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# Build wheel + sdist
|
|
158
|
+
uv build
|
|
159
|
+
|
|
160
|
+
# Output will be in dist/
|
|
161
|
+
ls dist/
|
|
162
|
+
# aicademy-0.1.0-py3-none-any.whl
|
|
163
|
+
# aicademy-0.1.0.tar.gz
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Publishing to PyPI
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Test on TestPyPI first
|
|
170
|
+
uv publish --index testpypi
|
|
171
|
+
|
|
172
|
+
# Publish to production PyPI
|
|
173
|
+
uv publish
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Security
|
|
177
|
+
|
|
178
|
+
- CLI tokens stored in `~/.aicademy/config.json`
|
|
179
|
+
- Tokens expire after 7 days — run `aicademy login` to renew
|
|
180
|
+
- Question tasks and scenarios only delivered when you have an active session (anti-scraping)
|
|
181
|
+
- Revoke all tokens with `aicademy logout`
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT © [Aicademy](https://www.aicademy.ac)
|
aicademy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Aicademy CLI
|
|
2
|
+
|
|
3
|
+
> Practice CKA, CKAD, and CKS exam scenarios locally — powered by KIND + Aicademy API.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/aicademy/)
|
|
6
|
+
[](https://pypi.org/project/aicademy/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Using pip
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install aicademy
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Using uv (recommended)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
uv tool install aicademy
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Using pipx
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pipx install aicademy
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# 1. Login to Aicademy
|
|
33
|
+
aicademy login
|
|
34
|
+
|
|
35
|
+
# 2. Check / install prerequisites
|
|
36
|
+
aicademy install-tool all --check
|
|
37
|
+
aicademy install-tool all
|
|
38
|
+
|
|
39
|
+
# 3. Start a practice question (creates KIND cluster automatically)
|
|
40
|
+
aicademy question start cka-01
|
|
41
|
+
|
|
42
|
+
# 4. Read the full task instructions in your terminal
|
|
43
|
+
aicademy question instructions
|
|
44
|
+
|
|
45
|
+
# 5. Solve the scenario using kubectl, helm, etc.
|
|
46
|
+
|
|
47
|
+
# 6. Verify your solution
|
|
48
|
+
aicademy verify
|
|
49
|
+
|
|
50
|
+
# 7. Clean up the cluster
|
|
51
|
+
aicademy question clear
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Command Reference
|
|
55
|
+
|
|
56
|
+
| Command | Description |
|
|
57
|
+
| ------------------------------------------- | ------------------------------------------------- |
|
|
58
|
+
| `aicademy login` | Authenticate (browser flow or direct token) |
|
|
59
|
+
| `aicademy logout` | Clear stored credentials |
|
|
60
|
+
| `aicademy auth whoami` | Verify token validity |
|
|
61
|
+
| `aicademy question start <id>` | Start question environment (creates KIND cluster) |
|
|
62
|
+
| `aicademy question instructions [id]` | Show full task instructions in terminal |
|
|
63
|
+
| `aicademy question instructions [id] --web` | Open question page in browser |
|
|
64
|
+
| `aicademy question clear [id]` | Delete KIND cluster and clear session |
|
|
65
|
+
| `aicademy verify [id]` | Run verify.sh and report result |
|
|
66
|
+
| `aicademy install-tool <name>` | Install kubectl / kind / docker / all |
|
|
67
|
+
| `aicademy install-tool <name> --check` | Check if tool is installed (no install) |
|
|
68
|
+
| `aicademy install-tool <name> --dry-run` | Preview install commands |
|
|
69
|
+
|
|
70
|
+
## Prerequisites
|
|
71
|
+
|
|
72
|
+
| Tool | Purpose | Install |
|
|
73
|
+
| ------- | ----------------- | ------------------------------- |
|
|
74
|
+
| Docker | Runs KIND nodes | `aicademy install-tool docker` |
|
|
75
|
+
| kubectl | Kubernetes CLI | `aicademy install-tool kubectl` |
|
|
76
|
+
| kind | Local K8s cluster | `aicademy install-tool kind` |
|
|
77
|
+
|
|
78
|
+
## OS Support
|
|
79
|
+
|
|
80
|
+
| OS | Package Manager |
|
|
81
|
+
| ---------| ------------------------|
|
|
82
|
+
| Windows | winget |
|
|
83
|
+
| macOS | Homebrew |
|
|
84
|
+
| Linux | Official shell scripts |
|
|
85
|
+
|
|
86
|
+
## Categories
|
|
87
|
+
|
|
88
|
+
| Exam | Slug | Questions | Free |
|
|
89
|
+
| --------------------------------------------| --------| -----------| ------|
|
|
90
|
+
| Certified Kubernetes Administrator | `cka` | 20 | 10 |
|
|
91
|
+
| Certified Kubernetes Application Developer | `ckad` | 20 | 10 |
|
|
92
|
+
| Certified Kubernetes Security Specialist | `cks` | 20 | 10 |
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
### Project Structure
|
|
97
|
+
|
|
98
|
+
The codebase is organized modularly:
|
|
99
|
+
|
|
100
|
+
- `aicademy_cli/main.py`: The entry point and top-level Typer application.
|
|
101
|
+
- `aicademy_cli/commands/`: All user-facing Typer CLI groups (`auth`, `question`, `tools`, `verify`).
|
|
102
|
+
- `aicademy_cli/api.py`: Centralized HTTP requests and error handling.
|
|
103
|
+
- `aicademy_cli/core/`: Internal logic like cluster management (`kind.py`) and helper methods (`utils.py`).
|
|
104
|
+
|
|
105
|
+
### Using uv
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Clone and install in dev mode
|
|
109
|
+
git clone https://github.com/devcrypted/aicademy-cli
|
|
110
|
+
cd aicademy-cli
|
|
111
|
+
uv sync
|
|
112
|
+
|
|
113
|
+
# Run against local dev server
|
|
114
|
+
AICADEMY_API_URL=http://localhost:5173 uv run aicademy login
|
|
115
|
+
|
|
116
|
+
# Run tests
|
|
117
|
+
uv run pytest
|
|
118
|
+
|
|
119
|
+
# Lint
|
|
120
|
+
uv run ruff check .
|
|
121
|
+
uv run mypy aicademy_cli/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Building the wheel
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Build wheel + sdist
|
|
128
|
+
uv build
|
|
129
|
+
|
|
130
|
+
# Output will be in dist/
|
|
131
|
+
ls dist/
|
|
132
|
+
# aicademy-0.1.0-py3-none-any.whl
|
|
133
|
+
# aicademy-0.1.0.tar.gz
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Publishing to PyPI
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Test on TestPyPI first
|
|
140
|
+
uv publish --index testpypi
|
|
141
|
+
|
|
142
|
+
# Publish to production PyPI
|
|
143
|
+
uv publish
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Security
|
|
147
|
+
|
|
148
|
+
- CLI tokens stored in `~/.aicademy/config.json`
|
|
149
|
+
- Tokens expire after 7 days — run `aicademy login` to renew
|
|
150
|
+
- Question tasks and scenarios only delivered when you have an active session (anti-scraping)
|
|
151
|
+
- Revoke all tokens with `aicademy logout`
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT © [Aicademy](https://www.aicademy.ac)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Aicademy CLI — Kubernetes exam practice in your terminal"""
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""API Client for Aicademy CLI"""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
from typing import Any
|
|
5
|
+
from . import config
|
|
6
|
+
|
|
7
|
+
class APIError(Exception):
|
|
8
|
+
def __init__(self, message: str, status_code: int, response_data: dict | str = None):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
self.response_data = response_data
|
|
12
|
+
|
|
13
|
+
def _get_headers(token: str | None = None) -> dict[str, str]:
|
|
14
|
+
t = token or config.get_token()
|
|
15
|
+
if t:
|
|
16
|
+
return {"Authorization": f"Bearer {t}"}
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
def _request(method: str, url: str, **kwargs) -> dict:
|
|
20
|
+
try:
|
|
21
|
+
resp = httpx.request(method, url, **kwargs)
|
|
22
|
+
except httpx.RequestError as exc:
|
|
23
|
+
raise APIError(f"Network error: {exc}", 0, str(exc))
|
|
24
|
+
|
|
25
|
+
if resp.status_code >= 400:
|
|
26
|
+
data = resp.text
|
|
27
|
+
try:
|
|
28
|
+
data = resp.json()
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
raise APIError(f"HTTP {resp.status_code}: {resp.reason_phrase}", resp.status_code, data)
|
|
32
|
+
|
|
33
|
+
if resp.status_code == 204:
|
|
34
|
+
return None
|
|
35
|
+
try:
|
|
36
|
+
return resp.json()
|
|
37
|
+
except Exception:
|
|
38
|
+
return resp.text
|
|
39
|
+
|
|
40
|
+
# ─── Auth ───────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def verify_token(token: str) -> dict:
|
|
43
|
+
return _request(
|
|
44
|
+
"POST",
|
|
45
|
+
f"{config.API_BASE_URL}/api/auth/cli-token",
|
|
46
|
+
headers=_get_headers(token),
|
|
47
|
+
timeout=10,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def logout(token: str) -> None:
|
|
51
|
+
try:
|
|
52
|
+
httpx.delete(
|
|
53
|
+
f"{config.API_BASE_URL}/api/auth/cli-token",
|
|
54
|
+
headers=_get_headers(token),
|
|
55
|
+
timeout=5,
|
|
56
|
+
)
|
|
57
|
+
except httpx.RequestError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def get_sessions() -> dict:
|
|
61
|
+
return _request(
|
|
62
|
+
"GET",
|
|
63
|
+
f"{config.API_BASE_URL}/api/practice/sessions",
|
|
64
|
+
headers=_get_headers(),
|
|
65
|
+
timeout=10,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ─── Practice ───────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
def start_session(question_id: str, cluster_name: str) -> dict:
|
|
71
|
+
return _request(
|
|
72
|
+
"POST",
|
|
73
|
+
f"{config.API_BASE_URL}/api/practice/sessions",
|
|
74
|
+
json={"questionId": question_id, "clusterName": cluster_name},
|
|
75
|
+
headers=_get_headers(),
|
|
76
|
+
timeout=15,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def get_question(category: str, question_id: str) -> dict:
|
|
80
|
+
return _request(
|
|
81
|
+
"GET",
|
|
82
|
+
f"{config.API_BASE_URL}/api/practice/questions/{category}/{question_id}",
|
|
83
|
+
headers=_get_headers(),
|
|
84
|
+
timeout=10,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def abandon_session(session_id: str) -> None:
|
|
88
|
+
try:
|
|
89
|
+
httpx.patch(
|
|
90
|
+
f"{config.API_BASE_URL}/api/practice/sessions/{session_id}",
|
|
91
|
+
headers=_get_headers(),
|
|
92
|
+
timeout=10,
|
|
93
|
+
)
|
|
94
|
+
except httpx.RequestError:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
def verify_session(session_id: str, result: dict) -> None:
|
|
98
|
+
try:
|
|
99
|
+
httpx.post(
|
|
100
|
+
f"{config.API_BASE_URL}/api/practice/sessions/{session_id}/verify",
|
|
101
|
+
json=result,
|
|
102
|
+
headers=_get_headers(),
|
|
103
|
+
timeout=10,
|
|
104
|
+
)
|
|
105
|
+
except httpx.RequestError:
|
|
106
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules for Aicademy."""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Authentication commands — login & logout"""
|
|
2
|
+
|
|
3
|
+
import webbrowser
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
from .. import config, api
|
|
9
|
+
from ..core import utils
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
app = typer.Typer(help="Authenticate with Aicademy")
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def login(
|
|
16
|
+
token: str = typer.Option(
|
|
17
|
+
None,
|
|
18
|
+
"--token",
|
|
19
|
+
"-t",
|
|
20
|
+
help="Paste a CLI token directly (skip browser flow)",
|
|
21
|
+
),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Login to Aicademy and store your CLI token."""
|
|
24
|
+
cfg = config.get_config()
|
|
25
|
+
if cfg.get("token"):
|
|
26
|
+
console.print(
|
|
27
|
+
"[yellow]ℹ Already logged in.[/yellow] Use [bold]aicademy logout[/bold] first to switch accounts."
|
|
28
|
+
)
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
if not token:
|
|
32
|
+
console.print(
|
|
33
|
+
Panel(
|
|
34
|
+
"[bold]Aicademy Login[/bold]\n\n"
|
|
35
|
+
"Opening your browser to generate a CLI token.\n"
|
|
36
|
+
"After logging in, copy the token and paste it below.",
|
|
37
|
+
title="🔐 Authentication",
|
|
38
|
+
border_style="cyan",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
token_url = f"{config.API_BASE_URL}/auth?cli=1"
|
|
42
|
+
console.print(f"\n [dim]→ Opening:[/dim] [cyan]{token_url}[/cyan]\n")
|
|
43
|
+
webbrowser.open(token_url)
|
|
44
|
+
token = Prompt.ask(" [bold]Paste your CLI token here[/bold]").strip()
|
|
45
|
+
|
|
46
|
+
if not token:
|
|
47
|
+
console.print("[red]✗ No token provided. Login cancelled.[/red]")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
# Verify the token against the API
|
|
51
|
+
console.print("\n[dim]Verifying token...[/dim]")
|
|
52
|
+
try:
|
|
53
|
+
api.verify_token(token)
|
|
54
|
+
except api.APIError as e:
|
|
55
|
+
if e.status_code == 401:
|
|
56
|
+
console.print("[red]✗ Token is invalid or expired. Please try again.[/red]")
|
|
57
|
+
else:
|
|
58
|
+
utils.format_access_error(e)
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
cfg["token"] = token
|
|
62
|
+
config.save_config(cfg)
|
|
63
|
+
console.print(
|
|
64
|
+
Panel(
|
|
65
|
+
"[bold green]✓ Logged in successfully![/bold green]\n\n"
|
|
66
|
+
f"Your token is stored in [dim]~/.aicademy/config.json[/dim]\n"
|
|
67
|
+
"It expires in [bold]7 days[/bold]. Run [bold]aicademy login[/bold] again to renew.",
|
|
68
|
+
title="✅ Success",
|
|
69
|
+
border_style="green",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def logout() -> None:
|
|
75
|
+
"""Log out and clear stored credentials."""
|
|
76
|
+
cfg = config.get_config()
|
|
77
|
+
if not cfg.get("token"):
|
|
78
|
+
console.print("[yellow]ℹ You are not logged in.[/yellow]")
|
|
79
|
+
raise typer.Exit()
|
|
80
|
+
|
|
81
|
+
token = cfg.get("token")
|
|
82
|
+
api.logout(token)
|
|
83
|
+
|
|
84
|
+
cfg.pop("token", None)
|
|
85
|
+
cfg.pop("active_session", None)
|
|
86
|
+
config.save_config(cfg)
|
|
87
|
+
console.print("[bold green]✓ Logged out successfully.[/bold green]")
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def whoami() -> None:
|
|
91
|
+
"""Show the currently logged-in user."""
|
|
92
|
+
token = utils.require_auth()
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
api.get_sessions()
|
|
96
|
+
console.print("[green]✓ Logged in[/green] — token is valid.")
|
|
97
|
+
except api.APIError as e:
|
|
98
|
+
if e.status_code == 401:
|
|
99
|
+
console.print("[red]Token expired.[/red] Please run [bold]aicademy login[/bold] again.")
|
|
100
|
+
else:
|
|
101
|
+
utils.format_access_error(e)
|
|
102
|
+
raise typer.Exit(1)
|