talk-python-cli 0.1.2__tar.gz → 0.2.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.
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/.claude/settings.local.json +4 -1
- talk_python_cli-0.2.0/CLAUDE.md +150 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/PKG-INFO +34 -18
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/README.md +33 -17
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/change-log.md +15 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/pyproject.toml +1 -1
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/__init__.py +1 -1
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/app.py +2 -2
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/client.py +2 -2
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/formatting.py +7 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/test_client.py +29 -0
- talk_python_cli-0.2.0/tests/test_formatting.py +38 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/uv.lock +1 -1
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/.gitignore +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/LICENSE +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/plans/001-talk-python-cli.plan.md +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/pyrefly.toml +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/ruff.toml +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/__main__.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/courses.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/episodes.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/src/talk_python_cli/guests.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/__init__.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/conftest.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/test_courses.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/test_episodes.py +0 -0
- {talk_python_cli-0.1.2 → talk_python_cli-0.2.0}/tests/test_guests.py +0 -0
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
"Bash(./venv/bin/python -m talk_python_cli.app status:*)",
|
|
6
6
|
"Bash(./venv/bin/talkpython status:*)",
|
|
7
7
|
"Bash(./venv/bin/pip install:*)",
|
|
8
|
-
"Bash(./venv/bin/talkpython:*)"
|
|
8
|
+
"Bash(./venv/bin/talkpython:*)",
|
|
9
|
+
"Bash(gh issue view:*)",
|
|
10
|
+
"Bash(pyrefly check:*)",
|
|
11
|
+
"Bash(uv lock:*)"
|
|
9
12
|
]
|
|
10
13
|
}
|
|
11
14
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# CLAUDE.md — Agent Instructions for talk-python-cli
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
CLI client for the Talk Python to Me podcast and Talk Python Training courses.
|
|
6
|
+
Wraps a remote MCP server (`https://talkpython.fm/api/mcp`) using JSON-RPC 2.0
|
|
7
|
+
over HTTP. The CLI is a **thin client** — all business logic lives on the server.
|
|
8
|
+
The CLI handles argument parsing, HTTP transport, and output formatting.
|
|
9
|
+
|
|
10
|
+
Published on PyPI as `talk-python-cli`. Entry point: `talkpython`.
|
|
11
|
+
|
|
12
|
+
## Critical Rules
|
|
13
|
+
|
|
14
|
+
- **Use `uv pip install`, never `pip install`.**
|
|
15
|
+
- **Virtual environment is `./venv`, NOT `./.venv`.**
|
|
16
|
+
- **After every code edit, run: `ruff format && ruff check --fix`**
|
|
17
|
+
- **Use `pyrefly check` to validate type information after changes.**
|
|
18
|
+
- Do not add unnecessary abstractions, comments, or docstrings to unchanged code.
|
|
19
|
+
- **Update `change-log.md` after every major change** (new features, breaking changes, notable fixes). Follow the [Keep a Changelog](https://keepachangelog.com/) format already in use.
|
|
20
|
+
|
|
21
|
+
## Build & Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Activate venv
|
|
25
|
+
source venv/bin/activate
|
|
26
|
+
|
|
27
|
+
# Install in editable mode
|
|
28
|
+
uv pip install -e ".[dev]"
|
|
29
|
+
|
|
30
|
+
# Run CLI
|
|
31
|
+
talkpython --help
|
|
32
|
+
talkpython episodes search "fastapi"
|
|
33
|
+
talkpython status
|
|
34
|
+
|
|
35
|
+
# Run tests
|
|
36
|
+
pytest
|
|
37
|
+
|
|
38
|
+
# Lint & format (ALWAYS after edits)
|
|
39
|
+
ruff format && ruff check --fix
|
|
40
|
+
|
|
41
|
+
# Type check (ALWAYS after edits)
|
|
42
|
+
pyrefly check
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Project Structure
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
src/talk_python_cli/
|
|
49
|
+
__init__.py # Version from importlib.metadata
|
|
50
|
+
__main__.py # python -m entry point
|
|
51
|
+
app.py # Root Cyclopts app, global options, meta-handler, status cmd
|
|
52
|
+
client.py # MCPClient: httpx-based JSON-RPC 2.0 client
|
|
53
|
+
formatting.py # Rich output: Markdown panels (text) or JSON
|
|
54
|
+
episodes.py # Episode commands (search, get, list, recent, transcript)
|
|
55
|
+
guests.py # Guest commands (search, get, list)
|
|
56
|
+
courses.py # Course commands (search, get, list)
|
|
57
|
+
|
|
58
|
+
tests/
|
|
59
|
+
conftest.py # Shared fixtures, JSON-RPC response builders
|
|
60
|
+
test_client.py # MCPClient tests
|
|
61
|
+
test_episodes.py # Episode command tests
|
|
62
|
+
test_guests.py # Guest command tests
|
|
63
|
+
test_courses.py # Course command tests
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Architecture & Key Patterns
|
|
67
|
+
|
|
68
|
+
### CLI Framework: Cyclopts (not Click, not Typer)
|
|
69
|
+
|
|
70
|
+
- Root app in `app.py` with sub-apps for episodes, guests, courses.
|
|
71
|
+
- **Meta-app launcher** (`@app.meta.default`): intercepts all invocations to
|
|
72
|
+
process global options (`--format`, `--url`) before dispatching to subcommands.
|
|
73
|
+
- Parameters use `Annotated[type, cyclopts.Parameter(...)]` for docs/defaults.
|
|
74
|
+
- Cyclopts auto-converts snake_case commands to kebab-case (e.g. `transcript_vtt` → `transcript-vtt`).
|
|
75
|
+
|
|
76
|
+
### Client Pattern
|
|
77
|
+
|
|
78
|
+
- `MCPClient` in `client.py` wraps httpx for JSON-RPC 2.0 over HTTP.
|
|
79
|
+
- Lazy initialization: `_ensure_initialized()` runs MCP handshake on first call.
|
|
80
|
+
- Session ID tracked via `Mcp-Session-Id` response header.
|
|
81
|
+
- `call_tool(tool_name, arguments)` is the only public API for MCP tool calls.
|
|
82
|
+
- Output format sent as URL query param: `?format=json` when JSON mode.
|
|
83
|
+
- No authentication required (public API).
|
|
84
|
+
|
|
85
|
+
### Lazy Client Access (avoids circular imports)
|
|
86
|
+
|
|
87
|
+
Each command module retrieves the client via a local helper:
|
|
88
|
+
```python
|
|
89
|
+
def _client():
|
|
90
|
+
from talk_python_cli.app import get_client
|
|
91
|
+
return get_client()
|
|
92
|
+
```
|
|
93
|
+
This deferred import avoids circular dependency since `app.py` imports the command modules.
|
|
94
|
+
|
|
95
|
+
### Output Formatting
|
|
96
|
+
|
|
97
|
+
- `display(content, format)` in `formatting.py` routes to markdown or JSON renderer.
|
|
98
|
+
- Text mode: Rich Markdown panel with "Talk Python" theme (cyan border, monokai code).
|
|
99
|
+
- JSON mode on TTY: pretty-printed with syntax highlighting.
|
|
100
|
+
- JSON mode piped: compact single-line JSON for scripting.
|
|
101
|
+
|
|
102
|
+
### Adding a New Command
|
|
103
|
+
|
|
104
|
+
1. Add function in the appropriate module (`episodes.py`, `guests.py`, `courses.py`).
|
|
105
|
+
2. Decorate with `@sub_app.default` or just define as a regular function in the sub-app.
|
|
106
|
+
3. Call `_client().call_tool('tool_name', {'arg': value})` to invoke the MCP tool.
|
|
107
|
+
4. Pass result to `display(result, _client().output_format)`.
|
|
108
|
+
5. Add tests in the corresponding test file using `pytest-httpx` mocks.
|
|
109
|
+
|
|
110
|
+
### Adding a New Command Group
|
|
111
|
+
|
|
112
|
+
1. Create `src/talk_python_cli/newgroup.py` with a `cyclopts.App(name='newgroup')`.
|
|
113
|
+
2. Register in `app.py`: `app.command(newgroup.sub_app)`.
|
|
114
|
+
3. Create `tests/test_newgroup.py`.
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
- Framework: **pytest** with **pytest-httpx** for HTTP mocking.
|
|
119
|
+
- `conftest.py` provides helpers: `jsonrpc_result()`, `tool_result()`, `add_init_responses()`.
|
|
120
|
+
- Every test must call `add_init_responses(httpx_mock)` before making MCP client calls.
|
|
121
|
+
- Tests verify JSON-RPC request structure, argument passing, and response handling.
|
|
122
|
+
|
|
123
|
+
## Dependencies
|
|
124
|
+
|
|
125
|
+
| Package | Purpose |
|
|
126
|
+
|-------------|--------------------------------|
|
|
127
|
+
| cyclopts | CLI framework (commands, args) |
|
|
128
|
+
| httpx | HTTP client for MCP calls |
|
|
129
|
+
| rich | Terminal output formatting |
|
|
130
|
+
| pytest | Testing (dev) |
|
|
131
|
+
| pytest-httpx| HTTP mocking in tests (dev) |
|
|
132
|
+
|
|
133
|
+
Build system: **hatchling**. Package manager: **uv**.
|
|
134
|
+
|
|
135
|
+
## Config Files
|
|
136
|
+
|
|
137
|
+
- `pyproject.toml` — Package metadata, dependencies, entry points, build config
|
|
138
|
+
- `ruff.toml` — Line length 120, single quotes, target Python 3.14
|
|
139
|
+
- `pyrefly.toml` — Type checker config, search path includes `src/`
|
|
140
|
+
- `uv.lock` — Locked dependencies (committed)
|
|
141
|
+
|
|
142
|
+
## Style Conventions
|
|
143
|
+
|
|
144
|
+
- Line length: 120
|
|
145
|
+
- Quotes: single quotes
|
|
146
|
+
- Modern Python type syntax: `dict | None` not `Optional[dict]`
|
|
147
|
+
- `from __future__ import annotations` in all modules
|
|
148
|
+
- Private helpers prefixed with `_`
|
|
149
|
+
- Minimal docstrings: only on public functions/classes
|
|
150
|
+
- Python target: 3.12+ (currently targeting 3.14 in tooling)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: talk-python-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: CLI for the Talk Python to Me podcast and courses
|
|
5
5
|
Project-URL: Homepage, https://github.com/talkpython/talk-python-cli
|
|
6
6
|
Project-URL: Source, https://github.com/talkpython/talk-python-cli
|
|
@@ -44,12 +44,12 @@ Unlock 500+ episodes of [Talk Python to Me](https://talkpython.fm), full transcr
|
|
|
44
44
|
Requires Python 3.12+.
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
#
|
|
48
|
-
uvx --from talk-python-cli talkpython episodes recent
|
|
49
|
-
|
|
50
|
-
# Or install it permanently with uv
|
|
47
|
+
# Install it permanently with uv
|
|
51
48
|
uv tool install talk-python-cli
|
|
52
49
|
|
|
50
|
+
# Or try it instantly with uvx (no install needed)
|
|
51
|
+
uvx --from talk-python-cli talkpython episodes recent
|
|
52
|
+
|
|
53
53
|
# Or with pip
|
|
54
54
|
pip install talk-python-cli
|
|
55
55
|
```
|
|
@@ -58,6 +58,8 @@ This installs the `talkpython` command.
|
|
|
58
58
|
|
|
59
59
|
## Quick start
|
|
60
60
|
|
|
61
|
+
Copy this whole block and paste it into your terminal to see what you get.
|
|
62
|
+
|
|
61
63
|
```bash
|
|
62
64
|
# Search for episodes about FastAPI
|
|
63
65
|
talkpython episodes search "FastAPI"
|
|
@@ -109,41 +111,55 @@ talkpython courses list
|
|
|
109
111
|
|
|
110
112
|
## Output formats
|
|
111
113
|
|
|
112
|
-
The CLI
|
|
114
|
+
The CLI supports three output formats via `--format`:
|
|
113
115
|
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
Override the default with `--format`:
|
|
116
|
+
- **`text`** (default) — Rich-formatted Markdown with styled panels and color for human reading.
|
|
117
|
+
- **`json`** — Structured JSON, pretty-printed on a TTY or compact when piped.
|
|
118
|
+
- **`markdown`** — Raw Markdown output with no Rich formatting. Ideal for piping into AI agents, LLMs, and automation tools that consume Markdown natively.
|
|
118
119
|
|
|
119
120
|
```bash
|
|
120
121
|
# Force JSON output in the terminal
|
|
121
122
|
talkpython --format json episodes search "async"
|
|
122
123
|
|
|
124
|
+
# Raw Markdown for AI agents and LLM pipelines
|
|
125
|
+
talkpython --format markdown episodes get 535
|
|
126
|
+
|
|
123
127
|
# Force rich text output even when piping
|
|
124
128
|
talkpython --format text episodes recent | less -R
|
|
125
129
|
```
|
|
126
130
|
|
|
131
|
+
## Agentic AI and LLM integration
|
|
132
|
+
|
|
133
|
+
Use `--format markdown` when feeding output to AI agents, LLMs, or RAG pipelines. This gives you clean, raw Markdown without terminal styling — exactly what language models expect:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Feed an episode summary to an LLM
|
|
137
|
+
talkpython --format markdown episodes get 535 | llm "Summarize this podcast episode"
|
|
138
|
+
|
|
139
|
+
# Grab a transcript for RAG ingestion
|
|
140
|
+
talkpython --format markdown episodes transcript 535 | your-rag-pipeline ingest
|
|
141
|
+
|
|
142
|
+
# Pipe course details into an AI agent
|
|
143
|
+
talkpython --format markdown courses get 57 | your-agent process
|
|
144
|
+
```
|
|
145
|
+
|
|
127
146
|
## Piping JSON to other tools
|
|
128
147
|
|
|
129
|
-
|
|
148
|
+
The `--format json` output integrates naturally with tools like `jq` or your own scripts:
|
|
130
149
|
|
|
131
150
|
```bash
|
|
132
151
|
# Extract episode titles with jq
|
|
133
|
-
talkpython episodes search "testing" | jq '.title'
|
|
152
|
+
talkpython --format json episodes search "testing" | jq '.title'
|
|
134
153
|
|
|
135
|
-
#
|
|
136
|
-
talkpython episodes
|
|
137
|
-
|
|
138
|
-
# Grab a transcript for RAG ingestion
|
|
139
|
-
talkpython episodes transcript 535 | your-rag-pipeline ingest
|
|
154
|
+
# Process structured data in a script
|
|
155
|
+
talkpython --format json episodes recent | python process_episodes.py
|
|
140
156
|
```
|
|
141
157
|
|
|
142
158
|
## Global options
|
|
143
159
|
|
|
144
160
|
| Option | Description |
|
|
145
161
|
|--------|-------------|
|
|
146
|
-
| `--format text\|json` |
|
|
162
|
+
| `--format text\|json\|markdown` | Output format: `text` (rich), `json`, or `markdown` (raw) |
|
|
147
163
|
| `--url <mcp-url>` | Override the MCP server URL (default: `https://talkpython.fm/api/mcp`) |
|
|
148
164
|
| `--version`, `-V` | Show version |
|
|
149
165
|
|
|
@@ -17,12 +17,12 @@ Unlock 500+ episodes of [Talk Python to Me](https://talkpython.fm), full transcr
|
|
|
17
17
|
Requires Python 3.12+.
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
#
|
|
21
|
-
uvx --from talk-python-cli talkpython episodes recent
|
|
22
|
-
|
|
23
|
-
# Or install it permanently with uv
|
|
20
|
+
# Install it permanently with uv
|
|
24
21
|
uv tool install talk-python-cli
|
|
25
22
|
|
|
23
|
+
# Or try it instantly with uvx (no install needed)
|
|
24
|
+
uvx --from talk-python-cli talkpython episodes recent
|
|
25
|
+
|
|
26
26
|
# Or with pip
|
|
27
27
|
pip install talk-python-cli
|
|
28
28
|
```
|
|
@@ -31,6 +31,8 @@ This installs the `talkpython` command.
|
|
|
31
31
|
|
|
32
32
|
## Quick start
|
|
33
33
|
|
|
34
|
+
Copy this whole block and paste it into your terminal to see what you get.
|
|
35
|
+
|
|
34
36
|
```bash
|
|
35
37
|
# Search for episodes about FastAPI
|
|
36
38
|
talkpython episodes search "FastAPI"
|
|
@@ -82,41 +84,55 @@ talkpython courses list
|
|
|
82
84
|
|
|
83
85
|
## Output formats
|
|
84
86
|
|
|
85
|
-
The CLI
|
|
87
|
+
The CLI supports three output formats via `--format`:
|
|
86
88
|
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
Override the default with `--format`:
|
|
89
|
+
- **`text`** (default) — Rich-formatted Markdown with styled panels and color for human reading.
|
|
90
|
+
- **`json`** — Structured JSON, pretty-printed on a TTY or compact when piped.
|
|
91
|
+
- **`markdown`** — Raw Markdown output with no Rich formatting. Ideal for piping into AI agents, LLMs, and automation tools that consume Markdown natively.
|
|
91
92
|
|
|
92
93
|
```bash
|
|
93
94
|
# Force JSON output in the terminal
|
|
94
95
|
talkpython --format json episodes search "async"
|
|
95
96
|
|
|
97
|
+
# Raw Markdown for AI agents and LLM pipelines
|
|
98
|
+
talkpython --format markdown episodes get 535
|
|
99
|
+
|
|
96
100
|
# Force rich text output even when piping
|
|
97
101
|
talkpython --format text episodes recent | less -R
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
## Agentic AI and LLM integration
|
|
105
|
+
|
|
106
|
+
Use `--format markdown` when feeding output to AI agents, LLMs, or RAG pipelines. This gives you clean, raw Markdown without terminal styling — exactly what language models expect:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Feed an episode summary to an LLM
|
|
110
|
+
talkpython --format markdown episodes get 535 | llm "Summarize this podcast episode"
|
|
111
|
+
|
|
112
|
+
# Grab a transcript for RAG ingestion
|
|
113
|
+
talkpython --format markdown episodes transcript 535 | your-rag-pipeline ingest
|
|
114
|
+
|
|
115
|
+
# Pipe course details into an AI agent
|
|
116
|
+
talkpython --format markdown courses get 57 | your-agent process
|
|
117
|
+
```
|
|
118
|
+
|
|
100
119
|
## Piping JSON to other tools
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
The `--format json` output integrates naturally with tools like `jq` or your own scripts:
|
|
103
122
|
|
|
104
123
|
```bash
|
|
105
124
|
# Extract episode titles with jq
|
|
106
|
-
talkpython episodes search "testing" | jq '.title'
|
|
125
|
+
talkpython --format json episodes search "testing" | jq '.title'
|
|
107
126
|
|
|
108
|
-
#
|
|
109
|
-
talkpython episodes
|
|
110
|
-
|
|
111
|
-
# Grab a transcript for RAG ingestion
|
|
112
|
-
talkpython episodes transcript 535 | your-rag-pipeline ingest
|
|
127
|
+
# Process structured data in a script
|
|
128
|
+
talkpython --format json episodes recent | python process_episodes.py
|
|
113
129
|
```
|
|
114
130
|
|
|
115
131
|
## Global options
|
|
116
132
|
|
|
117
133
|
| Option | Description |
|
|
118
134
|
|--------|-------------|
|
|
119
|
-
| `--format text\|json` |
|
|
135
|
+
| `--format text\|json\|markdown` | Output format: `text` (rich), `json`, or `markdown` (raw) |
|
|
120
136
|
| `--url <mcp-url>` | Override the MCP server URL (default: `https://talkpython.fm/api/mcp`) |
|
|
121
137
|
| `--version`, `-V` | Show version |
|
|
122
138
|
|
|
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `--format markdown` output mode for raw Markdown without Rich formatting, ideal for AI agents, LLMs, and RAG pipelines
|
|
12
|
+
- New "Agentic AI and LLM integration" section in README
|
|
13
|
+
- `display_markdown_raw()` in `formatting.py` for plain stdout output
|
|
14
|
+
- Tests for markdown format: query param propagation, raw output, and display routing
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `--format` flag now accepts `text`, `json`, or `markdown` (was `text` or `json`)
|
|
18
|
+
- MCP client sends `?format=markdown` query param to server when markdown format is selected
|
|
19
|
+
- README "Output formats" and "Piping JSON to other tools" sections updated for the new format
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
8
23
|
## [0.1.2] - 2026-02-07
|
|
9
24
|
|
|
10
25
|
### Added
|
|
@@ -82,10 +82,10 @@ def status() -> None:
|
|
|
82
82
|
def launcher(
|
|
83
83
|
*tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
|
|
84
84
|
format: Annotated[
|
|
85
|
-
Literal['text', 'json'],
|
|
85
|
+
Literal['text', 'json', 'markdown'],
|
|
86
86
|
cyclopts.Parameter(
|
|
87
87
|
name='--format',
|
|
88
|
-
help="Output format: 'text' (rich Markdown) or '
|
|
88
|
+
help="Output format: 'text' (rich Markdown), 'json', or 'markdown' (raw).",
|
|
89
89
|
),
|
|
90
90
|
] = 'text',
|
|
91
91
|
url: Annotated[
|
|
@@ -46,8 +46,8 @@ class MCPClient:
|
|
|
46
46
|
return self._msg_id
|
|
47
47
|
|
|
48
48
|
def _url(self) -> str:
|
|
49
|
-
if self.output_format
|
|
50
|
-
return f'{self.base_url}?format=
|
|
49
|
+
if self.output_format in ('json', 'markdown'):
|
|
50
|
+
return f'{self.base_url}?format={self.output_format}'
|
|
51
51
|
return self.base_url
|
|
52
52
|
|
|
53
53
|
def _post(self, payload: dict) -> httpx.Response:
|
|
@@ -39,10 +39,17 @@ def display(content: str, output_format: str) -> None:
|
|
|
39
39
|
"""Route content to the appropriate renderer."""
|
|
40
40
|
if output_format == 'json':
|
|
41
41
|
display_json(content)
|
|
42
|
+
elif output_format == 'markdown':
|
|
43
|
+
display_markdown_raw(content)
|
|
42
44
|
else:
|
|
43
45
|
display_markdown(content)
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def display_markdown_raw(content: str) -> None:
|
|
49
|
+
"""Print raw Markdown content to stdout without any Rich formatting."""
|
|
50
|
+
print(content)
|
|
51
|
+
|
|
52
|
+
|
|
46
53
|
def display_markdown(content: str) -> None:
|
|
47
54
|
"""Render Markdown content with Rich, wrapped in a styled panel."""
|
|
48
55
|
md = Markdown(content, code_theme='monokai')
|
|
@@ -197,6 +197,35 @@ class TestOutputFormat:
|
|
|
197
197
|
assert 'format=json' in str(req.url)
|
|
198
198
|
client.close()
|
|
199
199
|
|
|
200
|
+
def test_markdown_format_adds_query_param(self, httpx_mock: HTTPXMock) -> None:
|
|
201
|
+
md_url = f'{DEFAULT_URL}?format=markdown'
|
|
202
|
+
client = MCPClient(base_url=DEFAULT_URL, output_format='markdown')
|
|
203
|
+
|
|
204
|
+
httpx_mock.add_response(
|
|
205
|
+
method='POST',
|
|
206
|
+
url=md_url,
|
|
207
|
+
json=jsonrpc_result(
|
|
208
|
+
1,
|
|
209
|
+
{
|
|
210
|
+
'protocolVersion': '2025-03-26',
|
|
211
|
+
'capabilities': {'tools': {}},
|
|
212
|
+
'serverInfo': {'name': 'test', 'version': '0.1'},
|
|
213
|
+
},
|
|
214
|
+
),
|
|
215
|
+
headers={'mcp-session-id': 's1'},
|
|
216
|
+
)
|
|
217
|
+
httpx_mock.add_response(method='POST', url=md_url, status_code=202, headers={'mcp-session-id': 's1'})
|
|
218
|
+
httpx_mock.add_response(
|
|
219
|
+
method='POST',
|
|
220
|
+
url=md_url,
|
|
221
|
+
json=tool_result(3, '# Episode 535\n\nSome markdown content'),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
client.call_tool('get_episodes')
|
|
225
|
+
for req in httpx_mock.get_requests():
|
|
226
|
+
assert 'format=markdown' in str(req.url)
|
|
227
|
+
client.close()
|
|
228
|
+
|
|
200
229
|
|
|
201
230
|
class TestContextManager:
|
|
202
231
|
"""Verify MCPClient works as a context manager."""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Tests for output formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from talk_python_cli.formatting import display, display_markdown_raw
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestDisplayMarkdownRaw:
|
|
9
|
+
def test_prints_raw_content(self, capsys) -> None:
|
|
10
|
+
display_markdown_raw('# Hello\n\nSome **bold** text')
|
|
11
|
+
|
|
12
|
+
captured = capsys.readouterr()
|
|
13
|
+
assert captured.out == '# Hello\n\nSome **bold** text\n'
|
|
14
|
+
|
|
15
|
+
def test_no_rich_markup_in_output(self, capsys) -> None:
|
|
16
|
+
display_markdown_raw('## Heading\n- item 1\n- item 2')
|
|
17
|
+
|
|
18
|
+
captured = capsys.readouterr()
|
|
19
|
+
# Raw markdown should appear verbatim — no Rich panel borders or styling
|
|
20
|
+
assert '## Heading' in captured.out
|
|
21
|
+
assert '- item 1' in captured.out
|
|
22
|
+
assert '╭' not in captured.out # no Rich panel border
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestDisplayRouting:
|
|
26
|
+
def test_markdown_format_routes_to_raw(self, capsys) -> None:
|
|
27
|
+
display('# Raw markdown', 'markdown')
|
|
28
|
+
|
|
29
|
+
captured = capsys.readouterr()
|
|
30
|
+
assert captured.out == '# Raw markdown\n'
|
|
31
|
+
|
|
32
|
+
def test_text_format_does_not_print_raw(self, capsys) -> None:
|
|
33
|
+
display('# Rendered', 'text')
|
|
34
|
+
|
|
35
|
+
captured = capsys.readouterr()
|
|
36
|
+
# Text mode uses Rich console, so raw '# Rendered' should NOT appear verbatim
|
|
37
|
+
# (Rich renders it as a heading without the # prefix)
|
|
38
|
+
assert '# Rendered' not in captured.out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|