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.
@@ -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/
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/aicademy)](https://pypi.org/project/aicademy/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/aicademy)](https://pypi.org/project/aicademy/)
37
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)
@@ -0,0 +1,155 @@
1
+ # Aicademy CLI
2
+
3
+ > Practice CKA, CKAD, and CKS exam scenarios locally — powered by KIND + Aicademy API.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/aicademy)](https://pypi.org/project/aicademy/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/aicademy)](https://pypi.org/project/aicademy/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)