talk-python-cli 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- talk_python_cli/__main__.py +5 -0
- talk_python_cli/app.py +91 -0
- talk_python_cli/client.py +144 -0
- talk_python_cli/courses.py +59 -0
- talk_python_cli/episodes.py +99 -0
- talk_python_cli/formatting.py +94 -0
- talk_python_cli/guests.py +56 -0
- talk_python_cli-0.1.0.dist-info/METADATA +25 -0
- talk_python_cli-0.1.0.dist-info/RECORD +13 -0
- talk_python_cli-0.1.0.dist-info/WHEEL +4 -0
- talk_python_cli-0.1.0.dist-info/entry_points.txt +2 -0
- talk_python_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
talk_python_cli/app.py
ADDED
|
@@ -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,0 +1,144 @@
|
|
|
1
|
+
"""MCP HTTP client — thin JSON-RPC 2.0 wrapper over httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from talk_python_cli import __version__
|
|
8
|
+
|
|
9
|
+
DEFAULT_URL = 'https://talkpython.fm/api/mcp'
|
|
10
|
+
_PROTOCOL_VERSION = '2025-03-26'
|
|
11
|
+
_CLIENT_INFO = {'name': 'talk-python-cli', 'version': __version__}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPError(Exception):
|
|
15
|
+
"""Raised when the MCP server returns a JSON-RPC error."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, code: int, message: str, data: object = None):
|
|
18
|
+
self.code = code
|
|
19
|
+
self.message = message
|
|
20
|
+
self.data = data
|
|
21
|
+
super().__init__(f'MCP error {code}: {message}')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MCPClient:
|
|
25
|
+
"""Synchronous client for the Talk Python MCP server (Streamable HTTP)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str = DEFAULT_URL, output_format: str = 'text'):
|
|
28
|
+
self.base_url = base_url.rstrip('/')
|
|
29
|
+
self.output_format = output_format
|
|
30
|
+
self._msg_id = 0
|
|
31
|
+
self._session_id: str | None = None
|
|
32
|
+
self._initialized = False
|
|
33
|
+
self._http = httpx.Client(
|
|
34
|
+
timeout=30.0,
|
|
35
|
+
headers={
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
'Accept': 'application/json, text/event-stream',
|
|
38
|
+
'User-Agent': f'talk-python-cli/{__version__}',
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# -- internal helpers -----------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def _next_id(self) -> int:
|
|
45
|
+
self._msg_id += 1
|
|
46
|
+
return self._msg_id
|
|
47
|
+
|
|
48
|
+
def _url(self) -> str:
|
|
49
|
+
if self.output_format == 'json':
|
|
50
|
+
return f'{self.base_url}?format=json'
|
|
51
|
+
return self.base_url
|
|
52
|
+
|
|
53
|
+
def _post(self, payload: dict) -> httpx.Response:
|
|
54
|
+
"""Send a JSON-RPC request, attaching session header if available."""
|
|
55
|
+
headers: dict[str, str] = {}
|
|
56
|
+
if self._session_id:
|
|
57
|
+
headers['Mcp-Session-Id'] = self._session_id
|
|
58
|
+
resp = self._http.post(self._url(), json=payload, headers=headers)
|
|
59
|
+
# Capture session id from response
|
|
60
|
+
if 'mcp-session-id' in resp.headers:
|
|
61
|
+
self._session_id = resp.headers['mcp-session-id']
|
|
62
|
+
resp.raise_for_status()
|
|
63
|
+
return resp
|
|
64
|
+
|
|
65
|
+
def _send_request(self, method: str, params: dict | None = None) -> dict:
|
|
66
|
+
"""Send a JSON-RPC *request* (expects a response with an id)."""
|
|
67
|
+
payload: dict = {
|
|
68
|
+
'jsonrpc': '2.0',
|
|
69
|
+
'id': self._next_id(),
|
|
70
|
+
'method': method,
|
|
71
|
+
}
|
|
72
|
+
if params is not None:
|
|
73
|
+
payload['params'] = params
|
|
74
|
+
|
|
75
|
+
resp = self._post(payload)
|
|
76
|
+
body = resp.json()
|
|
77
|
+
|
|
78
|
+
if 'error' in body:
|
|
79
|
+
err = body['error']
|
|
80
|
+
raise MCPError(err.get('code', -1), err.get('message', 'Unknown error'), err.get('data'))
|
|
81
|
+
|
|
82
|
+
return body.get('result', {})
|
|
83
|
+
|
|
84
|
+
def _send_notification(self, method: str, params: dict | None = None) -> None:
|
|
85
|
+
"""Send a JSON-RPC *notification* (no id, no response expected)."""
|
|
86
|
+
payload: dict = {
|
|
87
|
+
'jsonrpc': '2.0',
|
|
88
|
+
'method': method,
|
|
89
|
+
}
|
|
90
|
+
if params is not None:
|
|
91
|
+
payload['params'] = params
|
|
92
|
+
self._post(payload)
|
|
93
|
+
|
|
94
|
+
# -- MCP lifecycle --------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def _initialize(self) -> None:
|
|
97
|
+
"""Perform the MCP initialize handshake."""
|
|
98
|
+
self._send_request(
|
|
99
|
+
'initialize',
|
|
100
|
+
{
|
|
101
|
+
'protocolVersion': _PROTOCOL_VERSION,
|
|
102
|
+
'capabilities': {},
|
|
103
|
+
'clientInfo': _CLIENT_INFO,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
self._send_notification('notifications/initialized')
|
|
107
|
+
self._initialized = True
|
|
108
|
+
|
|
109
|
+
def _ensure_initialized(self) -> None:
|
|
110
|
+
if not self._initialized:
|
|
111
|
+
self._initialize()
|
|
112
|
+
|
|
113
|
+
# -- public API -----------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def call_tool(self, tool_name: str, arguments: dict | None = None) -> str:
|
|
116
|
+
"""Call an MCP tool and return the text content from the first result.
|
|
117
|
+
|
|
118
|
+
Returns the raw text string from the server (Markdown or JSON depending
|
|
119
|
+
on the ``output_format`` passed at construction time).
|
|
120
|
+
"""
|
|
121
|
+
self._ensure_initialized()
|
|
122
|
+
|
|
123
|
+
result = self._send_request(
|
|
124
|
+
'tools/call',
|
|
125
|
+
{
|
|
126
|
+
'name': tool_name,
|
|
127
|
+
'arguments': arguments or {},
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# MCP tools/call result: {"content": [{"type": "text", "text": "..."}]}
|
|
132
|
+
content_list = result.get('content', [])
|
|
133
|
+
texts = [item['text'] for item in content_list if item.get('type') == 'text']
|
|
134
|
+
return '\n'.join(texts)
|
|
135
|
+
|
|
136
|
+
def close(self) -> None:
|
|
137
|
+
"""Close the underlying HTTP client."""
|
|
138
|
+
self._http.close()
|
|
139
|
+
|
|
140
|
+
def __enter__(self) -> MCPClient:
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, *exc: object) -> None:
|
|
144
|
+
self.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Course commands — search, get, list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cyclopts
|
|
6
|
+
|
|
7
|
+
from talk_python_cli.formatting import display
|
|
8
|
+
|
|
9
|
+
courses_app = cyclopts.App(
|
|
10
|
+
name='courses',
|
|
11
|
+
help='Browse and search Talk Python Training courses.',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client():
|
|
16
|
+
from talk_python_cli.app import get_client
|
|
17
|
+
|
|
18
|
+
return get_client()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@courses_app.command
|
|
22
|
+
def search(query: str, *, course_id: int | None = None) -> None:
|
|
23
|
+
"""Search courses, chapters, and lectures by keyword.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
query
|
|
28
|
+
Keywords for course, chapter, or lecture titles.
|
|
29
|
+
course_id
|
|
30
|
+
Limit search to a specific course (optional).
|
|
31
|
+
"""
|
|
32
|
+
client = _client()
|
|
33
|
+
args: dict = {'query': query}
|
|
34
|
+
if course_id is not None:
|
|
35
|
+
args['course_id'] = course_id
|
|
36
|
+
content = client.call_tool('search_courses', args)
|
|
37
|
+
display(content, client.output_format)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@courses_app.command
|
|
41
|
+
def get(course_id: int) -> None:
|
|
42
|
+
"""Get full details for a course by ID, including chapters and lectures.
|
|
43
|
+
|
|
44
|
+
Parameters
|
|
45
|
+
----------
|
|
46
|
+
course_id
|
|
47
|
+
The course ID.
|
|
48
|
+
"""
|
|
49
|
+
client = _client()
|
|
50
|
+
content = client.call_tool('get_course_details', {'course_id': course_id})
|
|
51
|
+
display(content, client.output_format)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@courses_app.command(name='list')
|
|
55
|
+
def list_courses() -> None:
|
|
56
|
+
"""List all available Talk Python Training courses."""
|
|
57
|
+
client = _client()
|
|
58
|
+
content = client.call_tool('get_courses')
|
|
59
|
+
display(content, client.output_format)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Episode commands — search, get, list, recent, transcript."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cyclopts
|
|
6
|
+
|
|
7
|
+
from talk_python_cli.formatting import display
|
|
8
|
+
|
|
9
|
+
episodes_app = cyclopts.App(
|
|
10
|
+
name='episodes',
|
|
11
|
+
help='Browse and search Talk Python to Me podcast episodes.',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client():
|
|
16
|
+
from talk_python_cli.app import get_client
|
|
17
|
+
|
|
18
|
+
return get_client()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@episodes_app.command
|
|
22
|
+
def search(query: str, *, limit: int = 10) -> None:
|
|
23
|
+
"""Search episodes by keyword.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
query
|
|
28
|
+
Search keywords for episode titles and descriptions.
|
|
29
|
+
limit
|
|
30
|
+
Maximum number of results to return.
|
|
31
|
+
"""
|
|
32
|
+
client = _client()
|
|
33
|
+
args = {'query': query, 'limit': limit}
|
|
34
|
+
content = client.call_tool('search_episodes', args)
|
|
35
|
+
display(content, client.output_format)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@episodes_app.command
|
|
39
|
+
def get(show_id: int) -> None:
|
|
40
|
+
"""Get full details for an episode by its show ID.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
show_id
|
|
45
|
+
Episode show ID (e.g. 400).
|
|
46
|
+
"""
|
|
47
|
+
client = _client()
|
|
48
|
+
content = client.call_tool('get_episode', {'show_id': show_id})
|
|
49
|
+
display(content, client.output_format)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@episodes_app.command(name='list')
|
|
53
|
+
def list_episodes() -> None:
|
|
54
|
+
"""List all podcast episodes with their show IDs and titles."""
|
|
55
|
+
client = _client()
|
|
56
|
+
content = client.call_tool('get_episodes')
|
|
57
|
+
display(content, client.output_format)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@episodes_app.command
|
|
61
|
+
def recent(*, limit: int = 10) -> None:
|
|
62
|
+
"""Get the most recently published episodes.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
limit
|
|
67
|
+
Number of recent episodes to return.
|
|
68
|
+
"""
|
|
69
|
+
client = _client()
|
|
70
|
+
content = client.call_tool('get_recent_episodes', {'limit': limit})
|
|
71
|
+
display(content, client.output_format)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@episodes_app.command
|
|
75
|
+
def transcript(show_id: int) -> None:
|
|
76
|
+
"""Get the full plain-text transcript for an episode.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
show_id
|
|
81
|
+
Episode show ID.
|
|
82
|
+
"""
|
|
83
|
+
client = _client()
|
|
84
|
+
content = client.call_tool('get_transcript_for_episode', {'show_id': show_id})
|
|
85
|
+
display(content, client.output_format)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@episodes_app.command(name='transcript-vtt')
|
|
89
|
+
def transcript_vtt(show_id: int) -> None:
|
|
90
|
+
"""Get the transcript in WebVTT format (with timestamps).
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
show_id
|
|
95
|
+
Episode show ID.
|
|
96
|
+
"""
|
|
97
|
+
client = _client()
|
|
98
|
+
content = client.call_tool('get_transcript_vtt', {'show_id': show_id})
|
|
99
|
+
display(content, client.output_format)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Output formatting — Rich Markdown rendering and JSON display."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.theme import Theme
|
|
13
|
+
|
|
14
|
+
# ── Custom theme for Talk Python branding ────────────────────────────────────
|
|
15
|
+
_TALK_PYTHON_THEME = Theme(
|
|
16
|
+
{
|
|
17
|
+
'tp.title': 'bold cyan',
|
|
18
|
+
'tp.heading': 'bold magenta',
|
|
19
|
+
'tp.id': 'bold yellow',
|
|
20
|
+
'tp.url': 'blue underline',
|
|
21
|
+
'tp.date': 'green',
|
|
22
|
+
'tp.dim': 'dim',
|
|
23
|
+
'tp.error': 'bold red',
|
|
24
|
+
'tp.success': 'bold green',
|
|
25
|
+
'tp.label': 'bold white',
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console(theme=_TALK_PYTHON_THEME, highlight=False)
|
|
30
|
+
error_console = Console(theme=_TALK_PYTHON_THEME, stderr=True, highlight=False)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_tty() -> bool:
|
|
34
|
+
"""Return True if stdout is connected to a terminal."""
|
|
35
|
+
return sys.stdout.isatty()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def display(content: str, output_format: str) -> None:
|
|
39
|
+
"""Route content to the appropriate renderer."""
|
|
40
|
+
if output_format == 'json':
|
|
41
|
+
display_json(content)
|
|
42
|
+
else:
|
|
43
|
+
display_markdown(content)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def display_markdown(content: str) -> None:
|
|
47
|
+
"""Render Markdown content with Rich, wrapped in a styled panel."""
|
|
48
|
+
md = Markdown(content, code_theme='monokai')
|
|
49
|
+
|
|
50
|
+
panel = Panel(
|
|
51
|
+
md,
|
|
52
|
+
border_style='cyan',
|
|
53
|
+
title='[bold cyan]Talk Python[/bold cyan]',
|
|
54
|
+
title_align='left',
|
|
55
|
+
padding=(1, 2),
|
|
56
|
+
)
|
|
57
|
+
console.print(panel)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def display_json(content: str) -> None:
|
|
61
|
+
"""Output JSON content — pretty-printed if on a TTY, raw otherwise."""
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(content)
|
|
64
|
+
except json.JSONDecodeError, TypeError:
|
|
65
|
+
# Server may have returned Markdown even though JSON was requested;
|
|
66
|
+
# fall back to printing the raw text.
|
|
67
|
+
console.print(content)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if is_tty():
|
|
71
|
+
from rich.syntax import Syntax
|
|
72
|
+
|
|
73
|
+
formatted = json.dumps(data, indent=2, ensure_ascii=False)
|
|
74
|
+
syntax = Syntax(formatted, 'json', theme='monokai', word_wrap=True)
|
|
75
|
+
console.print(syntax)
|
|
76
|
+
else:
|
|
77
|
+
# Raw compact JSON for piping
|
|
78
|
+
print(json.dumps(data, ensure_ascii=False))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def print_error(message: str) -> None:
|
|
82
|
+
"""Print a styled error message to stderr."""
|
|
83
|
+
error_console.print(
|
|
84
|
+
Text.assemble(
|
|
85
|
+
('ERROR', 'bold red'),
|
|
86
|
+
(' ', ''),
|
|
87
|
+
(message, 'red'),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def print_info(message: str) -> None:
|
|
93
|
+
"""Print a styled informational message."""
|
|
94
|
+
console.print(f'[tp.dim]{message}[/tp.dim]')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Guest commands — search, get, list."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import cyclopts
|
|
6
|
+
|
|
7
|
+
from talk_python_cli.formatting import display
|
|
8
|
+
|
|
9
|
+
guests_app = cyclopts.App(
|
|
10
|
+
name='guests',
|
|
11
|
+
help='Browse and search Talk Python to Me podcast guests.',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _client():
|
|
16
|
+
from talk_python_cli.app import get_client
|
|
17
|
+
|
|
18
|
+
return get_client()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@guests_app.command
|
|
22
|
+
def search(query: str, *, limit: int = 10) -> None:
|
|
23
|
+
"""Search guests by name.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
query
|
|
28
|
+
Guest name or partial name to search for.
|
|
29
|
+
limit
|
|
30
|
+
Maximum number of results to return.
|
|
31
|
+
"""
|
|
32
|
+
client = _client()
|
|
33
|
+
content = client.call_tool('search_guests', {'query': query, 'limit': limit})
|
|
34
|
+
display(content, client.output_format)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@guests_app.command
|
|
38
|
+
def get(guest_id: int) -> None:
|
|
39
|
+
"""Get detailed info about a guest by their ID.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
guest_id
|
|
44
|
+
The guest ID.
|
|
45
|
+
"""
|
|
46
|
+
client = _client()
|
|
47
|
+
content = client.call_tool('get_guest_by_id', {'guest_id': guest_id})
|
|
48
|
+
display(content, client.output_format)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@guests_app.command(name='list')
|
|
52
|
+
def list_guests() -> None:
|
|
53
|
+
"""List all podcast guests, sorted by number of appearances."""
|
|
54
|
+
client = _client()
|
|
55
|
+
content = client.call_tool('get_guests')
|
|
56
|
+
display(content, client.output_format)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: talk-python-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the Talk Python to Me podcast and courses
|
|
5
|
+
Project-URL: Homepage, https://github.com/talkpython/talk-python-cli
|
|
6
|
+
Project-URL: Source, https://github.com/talkpython/talk-python-cli
|
|
7
|
+
Project-URL: Documentation, https://github.com/talkpython/talk-python-cli
|
|
8
|
+
Author-email: Michael Kennedy <michael@talkpython.fm>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Topic :: Education
|
|
21
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: cyclopts>=3.0
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
talk_python_cli/__init__.py,sha256=05NfNnHR3JpU6LYq_GWjTERnI7FInPp_pmF9L1qk7uI,100
|
|
2
|
+
talk_python_cli/__main__.py,sha256=CiR9VEA51QfEByCah9e1uvR7gqiFDoBmsRgFVWltjBU,100
|
|
3
|
+
talk_python_cli/app.py,sha256=gzfruXfL8pDxr8pEFCTrUKzGfZGKr3Af87jtiHNlfRs,3224
|
|
4
|
+
talk_python_cli/client.py,sha256=NR48azeJlfHYFMqikUSjfrFOmCVClyOlq4yd0hyvxHY,4778
|
|
5
|
+
talk_python_cli/courses.py,sha256=EgWFbhLFeBTotDHu3xZ4fyiJokgWY2ZLFVaDRTU6NNo,1478
|
|
6
|
+
talk_python_cli/episodes.py,sha256=7Mm6uZ_jzMV6JBPv-1xbmINo7eyI4ylKvsT4_xT3Umw,2453
|
|
7
|
+
talk_python_cli/formatting.py,sha256=Ww3pfIAK50Ms2MSSkKrKjvwam_5C3ltEB1klHRGeM8U,2699
|
|
8
|
+
talk_python_cli/guests.py,sha256=O5MpmnhbACx1czKbV_p9KZywRwEo4jxOM5y8oU3ix50,1314
|
|
9
|
+
talk_python_cli-0.1.0.dist-info/METADATA,sha256=SkfLFf8i5jH2whFa8DCKxufxZ6dvzXjB0xoscxwt_HU,1038
|
|
10
|
+
talk_python_cli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
talk_python_cli-0.1.0.dist-info/entry_points.txt,sha256=Q7eLneKcuKeoSY_Ps-RyMb8BF1q6QPytIZ0ZYm7fhBg,56
|
|
12
|
+
talk_python_cli-0.1.0.dist-info/licenses/LICENSE,sha256=E1rfm5mKKkNGtHcctSAevPdM0uk6DqdPlcM48R1jGM4,1068
|
|
13
|
+
talk_python_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Talk Python
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|