Skip to content

Commit e2d4ad6

Browse files
committed
Initial version of the talkpython CLI.
1 parent 6bec0a2 commit e2d4ad6

19 files changed

+1774
-2
lines changed

README.md

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,118 @@
1-
# talk-python-cli
2-
A CLI interface to run Inquiries against the TalkPythonToMe podcast data.
1+
# Talk Python CLI
2+
3+
A command-line interface for accessing [Talk Python to Me](https://talkpython.fm) podcast episodes, guest information, and [Talk Python Training](https://training.talkpython.fm) courses. Built on the Talk Python [MCP server](https://talkpython.fm/api/mcp), it gives you structured access to the full Talk Python catalog from your terminal.
4+
5+
## Why use this?
6+
7+
- **Automation** — Query episode data, guest info, and course catalogs from scripts and pipelines.
8+
- **LLM & AI integration** — Pipe JSON output directly into AI agents, RAG systems, or chat workflows.
9+
- **Quick lookups** — Search episodes, pull transcripts, and browse courses without leaving the terminal.
10+
11+
## Installation
12+
13+
Requires Python 3.12+.
14+
15+
```bash
16+
# With uv (recommended)
17+
uv tool install talk-python-cli
18+
19+
# With pip
20+
pip install talk-python-cli
21+
```
22+
23+
This installs the `talkpython` command.
24+
25+
## Quick start
26+
27+
```bash
28+
# Search for episodes about FastAPI
29+
talkpython episodes search "FastAPI"
30+
31+
# Get full details for a specific episode
32+
talkpython episodes get 535
33+
34+
# Pull the transcript for an episode
35+
talkpython episodes transcript 535
36+
37+
# List recent episodes
38+
talkpython episodes recent --limit 5
39+
40+
# Search for a guest
41+
talkpython guests search "Hynek"
42+
43+
# Browse all training courses
44+
talkpython courses list
45+
```
46+
47+
## Commands
48+
49+
### Episodes
50+
51+
| Command | Description |
52+
|---------|-------------|
53+
| `talkpython episodes search <query> [--limit N]` | Search episodes by keyword (default limit: 10) |
54+
| `talkpython episodes get <show_id>` | Get full details for an episode |
55+
| `talkpython episodes list` | List all episodes |
56+
| `talkpython episodes recent [--limit N]` | Get the most recent episodes |
57+
| `talkpython episodes transcript <show_id>` | Get the plain-text transcript |
58+
| `talkpython episodes transcript-vtt <show_id>` | Get the WebVTT transcript (with timestamps) |
59+
60+
### Guests
61+
62+
| Command | Description |
63+
|---------|-------------|
64+
| `talkpython guests search <query> [--limit N]` | Search guests by name |
65+
| `talkpython guests get <guest_id>` | Get details for a specific guest |
66+
| `talkpython guests list` | List all guests, sorted by number of appearances |
67+
68+
### Courses
69+
70+
| Command | Description |
71+
|---------|-------------|
72+
| `talkpython courses search <query> [--course_id N]` | Search courses, chapters, and lectures |
73+
| `talkpython courses get <course_id>` | Get full course details including chapters and lectures |
74+
| `talkpython courses list` | List all available training courses |
75+
76+
## Output formats
77+
78+
The CLI auto-detects the best output format:
79+
80+
- **Interactive terminal** — Rich-formatted Markdown with styled panels and color.
81+
- **Piped / redirected** — Compact JSON, ready for processing.
82+
83+
Override the default with `--format`:
84+
85+
```bash
86+
# Force JSON output in the terminal
87+
talkpython --format json episodes search "async"
88+
89+
# Force rich text output even when piping
90+
talkpython --format text episodes recent | less -R
91+
```
92+
93+
## Piping JSON to other tools
94+
95+
Because the CLI outputs JSON automatically when piped, it integrates naturally with tools like `jq`, `llm`, or your own scripts:
96+
97+
```bash
98+
# Extract episode titles with jq
99+
talkpython episodes search "testing" | jq '.title'
100+
101+
# Feed episode data into an LLM
102+
talkpython episodes get 535 | llm "Summarize this podcast episode"
103+
104+
# Grab a transcript for RAG ingestion
105+
talkpython episodes transcript 535 | your-rag-pipeline ingest
106+
```
107+
108+
## Global options
109+
110+
| Option | Description |
111+
|--------|-------------|
112+
| `--format text\|json` | Force output format (auto-detected by default) |
113+
| `--url <mcp-url>` | Override the MCP server URL (default: `https://talkpython.fm/api/mcp`) |
114+
| `--version`, `-V` | Show version |
115+
116+
## License
117+
118+
MIT

pyproject.toml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[project]
2+
name = "talk-python-cli"
3+
version = "0.1.0"
4+
description = "CLI for the Talk Python to Me podcast and courses"
5+
requires-python = ">=3.12"
6+
license = "MIT"
7+
authors = [
8+
{ name = "Michael Kennedy", email = "michael@talkpython.fm" },
9+
]
10+
dependencies = [
11+
"cyclopts>=3.0",
12+
"httpx>=0.27",
13+
"rich>=13.0",
14+
]
15+
16+
[project.scripts]
17+
talkpython = "talk_python_cli.app:main"
18+
19+
[dependency-groups]
20+
dev = [
21+
"pytest>=8.0",
22+
"pytest-httpx>=0.34",
23+
]
24+
25+
[tool.hatch.build.targets.wheel]
26+
packages = ["src/talk_python_cli"]
27+
28+
[build-system]
29+
requires = ["hatchling"]
30+
build-backend = "hatchling.build"

pyrefly.toml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
###### configuring what to type check and where to import from
2+
3+
# check all files in "."
4+
project-includes = ["."]
5+
# exclude dotfiles
6+
project-excludes = ["**/.[!/.]*", "**/*venv/**"]
7+
# perform an upward search for `.gitignore`, `.ignore`, and `.git/info/exclude`, and
8+
# add those to `project-excludes` automatically
9+
use-ignore-files = true
10+
# import project files from "src" (main package) and "." (tests)
11+
search-path = ["src", "."]
12+
# let Pyrefly try to guess your search path
13+
disable-search-path-heuristics = false
14+
# do not include any third-party packages (except those provided by an interpreter)
15+
site-package-path = []
16+
17+
###### configuring your python environment
18+
19+
python-platform = "darwin"
20+
# assume the Python version we're using is 3.14, without querying an interpreter
21+
python-version = "3.14"
22+
# is Pyrefly disallowed from querying for an interpreter to automatically determine your
23+
# `python-platform`, `python-version`, and extra entries to `site-package-path`?
24+
skip-interpreter-query = false
25+
# query the default Python interpreter on your system, if installed and `python_platform`,
26+
# `python-version`, or `site-package-path` are unset.
27+
# python-interpreter = null # this is commented out because there are no `null` values in TOML
28+
29+
#### configuring your type check settings
30+
31+
# wildcards for which Pyrefly will unconditionally replace the import with `typing.Any`
32+
replace-imports-with-any = []
33+
# wildcards for which Pyrefly will replace the import with `typing.Any` if it can't be found
34+
ignore-missing-imports = []
35+
# should Pyrefly skip type checking if we find a generated file?
36+
ignore-errors-in-generated-code = false
37+
# what should Pyrefly do when it encounters a function that is untyped?
38+
untyped-def-behavior = "check-and-infer-return-type"
39+
# can Pyrefly recognize ignore directives other than `# pyrefly: ignore` and `# type: ignore`
40+
permissive-ignores = false
41+
42+
[errors]
43+
# this is an empty table, meaning all errors are enabled by default
44+
45+
# no `[[sub-config]]` entries are included, since there are none by default

ruff.toml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# [ruff]
2+
line-length = 120
3+
format.quote-style = "single"
4+
5+
# Enable Pyflakes `E` and `F` codes by default.
6+
lint.select = ["E", "F", "I"]
7+
lint.ignore = []
8+
9+
# Exclude a variety of commonly ignored directories.
10+
exclude = [
11+
".bzr",
12+
".direnv",
13+
".eggs",
14+
".git",
15+
".hg",
16+
".mypy_cache",
17+
".nox",
18+
".pants.d",
19+
".ruff_cache",
20+
".svn",
21+
".tox",
22+
"__pypackages__",
23+
"_build",
24+
"buck-out",
25+
"build",
26+
"dist",
27+
"node_modules",
28+
".env",
29+
".venv",
30+
"venv",
31+
"typings/**/*.pyi",
32+
]
33+
lint.per-file-ignores = { }
34+
35+
# Allow unused variables when underscore-prefixed.
36+
# dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
37+
38+
# Assume Python 3.14.
39+
target-version = "py314"
40+
41+
#[tool.ruff.mccabe]
42+
## Unlike Flake8, default to a complexity level of 10.
43+
lint.mccabe.max-complexity = 10

src/talk_python_cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Talk Python to Me CLI — query podcast episodes, guests, and courses."""
2+
3+
__version__ = '0.1.0'

src/talk_python_cli/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Allow running as ``python -m talk_python_cli``."""
2+
3+
from talk_python_cli.app import main
4+
5+
main()

src/talk_python_cli/app.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Root Cyclopts application with global options."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from typing import Annotated, Literal
7+
8+
import cyclopts
9+
10+
from talk_python_cli import __version__
11+
from talk_python_cli.client import DEFAULT_URL, MCPClient
12+
from talk_python_cli.formatting import is_tty, print_error
13+
14+
# ── Shared state ─────────────────────────────────────────────────────────────
15+
# The meta-app handler stores the client here so command modules can access it.
16+
_client: MCPClient | None = None
17+
18+
19+
def get_client() -> MCPClient:
20+
"""Return the active MCPClient (set by the meta-app launcher)."""
21+
assert _client is not None, 'MCPClient not initialised — this is a bug'
22+
return _client
23+
24+
25+
# ── Root app ─────────────────────────────────────────────────────────────────
26+
app = cyclopts.App(
27+
name='talkpython',
28+
help='CLI for the Talk Python to Me podcast and courses.\n\n'
29+
'Query episodes, guests, transcripts, and training courses\n'
30+
'from the Talk Python MCP server.',
31+
version=__version__,
32+
version_flags=['--version', '-V'],
33+
)
34+
35+
# ── Register sub-apps (imported here to avoid circular imports) ──────────────
36+
from talk_python_cli.courses import courses_app # noqa: E402
37+
from talk_python_cli.episodes import episodes_app # noqa: E402
38+
from talk_python_cli.guests import guests_app # noqa: E402
39+
40+
app.command(episodes_app)
41+
app.command(guests_app)
42+
app.command(courses_app)
43+
44+
45+
# ── Meta-app: handles global options before dispatching to sub-commands ──────
46+
@app.meta.default
47+
def launcher(
48+
*tokens: Annotated[str, cyclopts.Parameter(show=False, allow_leading_hyphen=True)],
49+
format: Annotated[
50+
Literal['text', 'json'],
51+
cyclopts.Parameter(
52+
name='--format',
53+
help="Output format: 'text' (rich Markdown) or 'json'. Defaults to 'json' when stdout is piped.",
54+
),
55+
] = None, # type: ignore
56+
url: Annotated[
57+
str,
58+
cyclopts.Parameter(
59+
name='--url',
60+
help='MCP server URL.',
61+
show_default=True,
62+
),
63+
] = DEFAULT_URL,
64+
) -> None:
65+
global _client
66+
67+
# Auto-detect: default to json when piped, text when interactive
68+
if format is None:
69+
format = 'text' if is_tty() else 'json'
70+
71+
_client = MCPClient(base_url=url, output_format=format)
72+
try:
73+
app(tokens)
74+
except Exception as exc:
75+
print_error(str(exc))
76+
sys.exit(1)
77+
finally:
78+
_client.close()
79+
_client = None
80+
81+
82+
# ── Entrypoint ───────────────────────────────────────────────────────────────
83+
def main() -> None:
84+
"""CLI entrypoint — called by the ``talkpython`` console script."""
85+
try:
86+
app.meta()
87+
except SystemExit:
88+
raise
89+
except Exception as exc:
90+
print_error(str(exc))
91+
sys.exit(1)

0 commit comments

Comments
 (0)