thuk 1.0.1__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.
- thuk/__init__.py +4 -0
- thuk/__main__.py +2 -0
- thuk/cli.py +152 -0
- thuk/data/contents.json +330 -0
- thuk/data/django_code.pdf +0 -0
- thuk/data/django_code.txt +847 -0
- thuk/data/docker_commands.pdf +0 -0
- thuk/data/docker_commands.txt +660 -0
- thuk/data/git_commands.txt +65 -0
- thuk/data/unit2_theory.pdf +0 -0
- thuk/data/unit2_theory.txt +1161 -0
- thuk/data/unit5_theory.pdf +0 -0
- thuk/data/unit5_theory.txt +691 -0
- thuk/reference.py +91 -0
- thuk-1.0.1.dist-info/METADATA +100 -0
- thuk-1.0.1.dist-info/RECORD +19 -0
- thuk-1.0.1.dist-info/WHEEL +5 -0
- thuk-1.0.1.dist-info/entry_points.txt +2 -0
- thuk-1.0.1.dist-info/top_level.txt +1 -0
thuk/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""Thuk: offline study documents with contents and keyword search."""
|
|
2
|
+
from .reference import SECTIONS, get_text, search, list_topics, get_topic, search_topics
|
|
3
|
+
__version__ = '1.0.1'
|
|
4
|
+
__all__ = ['SECTIONS', 'get_text', 'search', 'list_topics', 'get_topic', 'search_topics']
|
thuk/__main__.py
ADDED
thuk/cli.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Nested document browser and keyword search."""
|
|
2
|
+
import argparse
|
|
3
|
+
import pydoc
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .reference import SECTIONS, get_text, get_topic, list_topics, search_topics, export_pdf, open_pdf
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def highlight(text, keyword, color=False):
|
|
12
|
+
if not keyword:
|
|
13
|
+
return text
|
|
14
|
+
pattern = re.compile(re.escape(keyword), re.IGNORECASE)
|
|
15
|
+
return pattern.sub(lambda m: ('\033[1;33m' + m[0] + '\033[0m') if color else '[[' + m[0] + ']]', text)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def display(section, topic=None, plain=False, keyword=None):
|
|
19
|
+
title = SECTIONS[section][0]
|
|
20
|
+
body = get_text(section) if topic is None else get_topic(section, topic)
|
|
21
|
+
if topic is not None:
|
|
22
|
+
title += ' / ' + list_topics(section)[topic - 1]['title']
|
|
23
|
+
# Markers remain visible even in Windows pagers that strip ANSI escapes.
|
|
24
|
+
text = title + '\n' + '=' * 60 + '\n\n' + highlight(body, keyword)
|
|
25
|
+
if keyword:
|
|
26
|
+
text = f'Search: {keyword} (matches marked with [[...]]).\n\n' + text
|
|
27
|
+
if plain or not sys.stdout.isatty():
|
|
28
|
+
print(text)
|
|
29
|
+
else:
|
|
30
|
+
pydoc.pager(text)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ask(prompt):
|
|
34
|
+
return input(prompt).strip()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def search_menu(section, plain=False, query=None):
|
|
38
|
+
if query is None:
|
|
39
|
+
query = ask('Keyword (blank to cancel): ')
|
|
40
|
+
if not query.strip():
|
|
41
|
+
return
|
|
42
|
+
results = search_topics(query, section)
|
|
43
|
+
if not results:
|
|
44
|
+
print('No matches found in this document.')
|
|
45
|
+
return
|
|
46
|
+
while True:
|
|
47
|
+
print(f'\nSearch results in {SECTIONS[section][0]} for "{query}"')
|
|
48
|
+
for i, result in enumerate(results, 1):
|
|
49
|
+
print(f"{i}. {result['title']} ({len(result['matches'])} matching lines)")
|
|
50
|
+
for line, snippet in result['matches'][:2]:
|
|
51
|
+
print(f' Line {line}: {highlight(snippet, query)}')
|
|
52
|
+
print('0. Back to document contents')
|
|
53
|
+
choice = ask('Open result: ')
|
|
54
|
+
if choice == '0':
|
|
55
|
+
return
|
|
56
|
+
if choice.isdigit() and 1 <= int(choice) <= len(results):
|
|
57
|
+
display(section, results[int(choice) - 1]['topic'], plain, query)
|
|
58
|
+
return
|
|
59
|
+
else:
|
|
60
|
+
print('Choose a result number or 0.')
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def document_menu(section, plain=False):
|
|
64
|
+
topics = list_topics(section)
|
|
65
|
+
while True:
|
|
66
|
+
print('\n' + SECTIONS[section][0] + ' - Contents\n' + '=' * 60)
|
|
67
|
+
for number, item in enumerate(topics, 1):
|
|
68
|
+
indent = ' ' if item['level'] == 2 else ''
|
|
69
|
+
print(f"{number}. {indent}{item['title']}")
|
|
70
|
+
print('S. Search this document')
|
|
71
|
+
print('0. Back to documents')
|
|
72
|
+
choice = ask('Select a section or S to search: ').lower()
|
|
73
|
+
if choice == '0':
|
|
74
|
+
return
|
|
75
|
+
if choice == 's':
|
|
76
|
+
search_menu(section, plain)
|
|
77
|
+
elif choice.isdigit() and 1 <= int(choice) <= len(topics):
|
|
78
|
+
display(section, int(choice), plain)
|
|
79
|
+
else:
|
|
80
|
+
print('Choose a section number, S, or 0.')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def menu(plain=False, pdf=False):
|
|
84
|
+
while True:
|
|
85
|
+
print('\nTHUK\n' + '=' * 42)
|
|
86
|
+
for number, (title, _) in SECTIONS.items():
|
|
87
|
+
print(f'{number}. {title}')
|
|
88
|
+
print('0. Exit\n' + '=' * 42)
|
|
89
|
+
choice = ask('Select a document (1-5): ')
|
|
90
|
+
if choice == '0':
|
|
91
|
+
return 0
|
|
92
|
+
if choice not in {str(n) for n in SECTIONS}:
|
|
93
|
+
print('Choose a document from 1 to 5, or 0 to exit.')
|
|
94
|
+
continue
|
|
95
|
+
if pdf:
|
|
96
|
+
try:
|
|
97
|
+
print(f'Opened: {open_pdf(int(choice))}')
|
|
98
|
+
except (ValueError, OSError, subprocess.CalledProcessError) as exc:
|
|
99
|
+
print(f'Cannot open PDF: {exc}')
|
|
100
|
+
else:
|
|
101
|
+
document_menu(int(choice), plain)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(argv=None):
|
|
105
|
+
parser = argparse.ArgumentParser(description='Thuk offline document browser')
|
|
106
|
+
parser.add_argument('section', nargs='?', type=int, choices=SECTIONS, help='document number (1-5)')
|
|
107
|
+
parser.add_argument('--topic', type=int, help='read one topic by its contents-menu number')
|
|
108
|
+
parser.add_argument('--version', action='version', version=f'Thuk {__version__}')
|
|
109
|
+
group = parser.add_mutually_exclusive_group()
|
|
110
|
+
group.add_argument('--pdf', action='store_true', help='open the original PDF')
|
|
111
|
+
group.add_argument('--export-pdf', metavar='PATH', help='save the original PDF')
|
|
112
|
+
group.add_argument('--search', metavar='TEXT', help='search the selected document, or all documents')
|
|
113
|
+
group.add_argument('--full', action='store_true', help='print the complete document')
|
|
114
|
+
parser.add_argument('--plain', action='store_true', help='disable the interactive pager')
|
|
115
|
+
args = parser.parse_args(argv)
|
|
116
|
+
if (args.topic is not None or args.export_pdf or args.full) and args.section is None:
|
|
117
|
+
parser.error('--topic, --export-pdf and --full require a document number')
|
|
118
|
+
if args.topic is not None and (args.pdf or args.export_pdf or args.full or args.search is not None):
|
|
119
|
+
parser.error('--topic cannot be combined with PDF, full-document or search options')
|
|
120
|
+
try:
|
|
121
|
+
if args.search is not None:
|
|
122
|
+
if not args.search.strip():
|
|
123
|
+
raise ValueError('Enter a non-empty search query.')
|
|
124
|
+
if args.section is not None and sys.stdin.isatty():
|
|
125
|
+
search_menu(args.section, args.plain, args.search)
|
|
126
|
+
document_menu(args.section, args.plain)
|
|
127
|
+
else:
|
|
128
|
+
found = False
|
|
129
|
+
for section in ([args.section] if args.section else SECTIONS):
|
|
130
|
+
for result in search_topics(args.search, section):
|
|
131
|
+
found = True
|
|
132
|
+
print(f"{SECTIONS[section][0]} / {result['title']} (topic {result['topic']})")
|
|
133
|
+
for line, snippet in result['matches']:
|
|
134
|
+
print(f' {line}: {highlight(snippet, args.search)}')
|
|
135
|
+
if not found:
|
|
136
|
+
print('No matches found.')
|
|
137
|
+
elif args.section is None:
|
|
138
|
+
return menu(args.plain, args.pdf)
|
|
139
|
+
elif args.export_pdf:
|
|
140
|
+
print(export_pdf(args.section, args.export_pdf))
|
|
141
|
+
elif args.pdf:
|
|
142
|
+
print(f'Opened: {open_pdf(args.section)}')
|
|
143
|
+
elif args.topic is not None or args.full:
|
|
144
|
+
display(args.section, args.topic, args.plain)
|
|
145
|
+
else:
|
|
146
|
+
document_menu(args.section, args.plain)
|
|
147
|
+
return 0
|
|
148
|
+
except (EOFError, KeyboardInterrupt, BrokenPipeError):
|
|
149
|
+
return 0
|
|
150
|
+
except (ValueError, OSError, subprocess.CalledProcessError) as exc:
|
|
151
|
+
print(f'Thuk: {exc}', file=sys.stderr)
|
|
152
|
+
return 1
|
thuk/data/contents.json
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
{
|
|
2
|
+
"1": [
|
|
3
|
+
{
|
|
4
|
+
"title": "Document introduction",
|
|
5
|
+
"start": 0,
|
|
6
|
+
"end": 4,
|
|
7
|
+
"level": 1
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"title": "1. Course Orientation",
|
|
11
|
+
"start": 4,
|
|
12
|
+
"end": 30,
|
|
13
|
+
"level": 1
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "2. Python Core",
|
|
17
|
+
"start": 30,
|
|
18
|
+
"end": 246,
|
|
19
|
+
"level": 1
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"title": "3. Networking and Files",
|
|
23
|
+
"start": 246,
|
|
24
|
+
"end": 423,
|
|
25
|
+
"level": 1
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"title": "4. Django MVT",
|
|
29
|
+
"start": 423,
|
|
30
|
+
"end": 685,
|
|
31
|
+
"level": 1
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"title": "5. Auth and Sessions",
|
|
35
|
+
"start": 685,
|
|
36
|
+
"end": 773,
|
|
37
|
+
"level": 1
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"title": "6. Database Connectivity",
|
|
41
|
+
"start": 773,
|
|
42
|
+
"end": 880,
|
|
43
|
+
"level": 1
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"title": "7. APIs with Django REST Framework",
|
|
47
|
+
"start": 880,
|
|
48
|
+
"end": 1011,
|
|
49
|
+
"level": 1
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"title": "8. Machine Learning Model Integration",
|
|
53
|
+
"start": 1011,
|
|
54
|
+
"end": 1120,
|
|
55
|
+
"level": 1
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"title": "9. Quick Recap",
|
|
59
|
+
"start": 1120,
|
|
60
|
+
"end": 1161,
|
|
61
|
+
"level": 1
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"2": [
|
|
65
|
+
{
|
|
66
|
+
"title": "Document introduction",
|
|
67
|
+
"start": 0,
|
|
68
|
+
"end": 25,
|
|
69
|
+
"level": 1
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"title": "1. Course Orientation",
|
|
73
|
+
"start": 25,
|
|
74
|
+
"end": 40,
|
|
75
|
+
"level": 1
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"title": "2. Why Docker Exists",
|
|
79
|
+
"start": 40,
|
|
80
|
+
"end": 120,
|
|
81
|
+
"level": 1
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"title": "3. Containers vs Virtual Machines",
|
|
85
|
+
"start": 120,
|
|
86
|
+
"end": 161,
|
|
87
|
+
"level": 1
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"title": "4. Docker Architecture",
|
|
91
|
+
"start": 161,
|
|
92
|
+
"end": 211,
|
|
93
|
+
"level": 1
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"title": "5. Images and the Dockerfile",
|
|
97
|
+
"start": 211,
|
|
98
|
+
"end": 308,
|
|
99
|
+
"level": 1
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"title": "6. Containers, Storage and Networking",
|
|
103
|
+
"start": 308,
|
|
104
|
+
"end": 422,
|
|
105
|
+
"level": 1
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"title": "7. Docker Compose",
|
|
109
|
+
"start": 422,
|
|
110
|
+
"end": 468,
|
|
111
|
+
"level": 1
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"title": "8. Integrating Django with Docker",
|
|
115
|
+
"start": 468,
|
|
116
|
+
"end": 500,
|
|
117
|
+
"level": 1
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"title": "9. Deployment and Security",
|
|
121
|
+
"start": 500,
|
|
122
|
+
"end": 595,
|
|
123
|
+
"level": 1
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"title": "10. Best Practice and Debugging",
|
|
127
|
+
"start": 595,
|
|
128
|
+
"end": 661,
|
|
129
|
+
"level": 1
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"title": "11. Quick Recap",
|
|
133
|
+
"start": 661,
|
|
134
|
+
"end": 691,
|
|
135
|
+
"level": 1
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"3": [
|
|
139
|
+
{
|
|
140
|
+
"title": "Document introduction",
|
|
141
|
+
"start": 0,
|
|
142
|
+
"end": 7,
|
|
143
|
+
"level": 1
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"title": "1. Python Core",
|
|
147
|
+
"start": 7,
|
|
148
|
+
"end": 127,
|
|
149
|
+
"level": 1
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"title": "2. Networking and Files",
|
|
153
|
+
"start": 127,
|
|
154
|
+
"end": 237,
|
|
155
|
+
"level": 1
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"title": "3. Django Project Setup",
|
|
159
|
+
"start": 237,
|
|
160
|
+
"end": 264,
|
|
161
|
+
"level": 1
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"title": "4. Django MVT",
|
|
165
|
+
"start": 264,
|
|
166
|
+
"end": 414,
|
|
167
|
+
"level": 1
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"title": "5. Auth and Sessions",
|
|
171
|
+
"start": 414,
|
|
172
|
+
"end": 516,
|
|
173
|
+
"level": 1
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"title": "6. Database Connectivity",
|
|
177
|
+
"start": 516,
|
|
178
|
+
"end": 589,
|
|
179
|
+
"level": 1
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"title": "7. Django REST Framework",
|
|
183
|
+
"start": 589,
|
|
184
|
+
"end": 687,
|
|
185
|
+
"level": 1
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"title": "8. Machine Learning Model Integration",
|
|
189
|
+
"start": 687,
|
|
190
|
+
"end": 782,
|
|
191
|
+
"level": 1
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"title": "9. Common Errors and Fixes",
|
|
195
|
+
"start": 782,
|
|
196
|
+
"end": 828,
|
|
197
|
+
"level": 1
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"title": "10. Command Sheet",
|
|
201
|
+
"start": 828,
|
|
202
|
+
"end": 847,
|
|
203
|
+
"level": 1
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"4": [
|
|
207
|
+
{
|
|
208
|
+
"title": "Document introduction",
|
|
209
|
+
"start": 0,
|
|
210
|
+
"end": 36,
|
|
211
|
+
"level": 1
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"title": "1. Setup",
|
|
215
|
+
"start": 36,
|
|
216
|
+
"end": 65,
|
|
217
|
+
"level": 1
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"title": "2. Images",
|
|
221
|
+
"start": 65,
|
|
222
|
+
"end": 107,
|
|
223
|
+
"level": 1
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"title": "3. Containers",
|
|
227
|
+
"start": 107,
|
|
228
|
+
"end": 167,
|
|
229
|
+
"level": 1
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"title": "4. The Dockerfile",
|
|
233
|
+
"start": 167,
|
|
234
|
+
"end": 293,
|
|
235
|
+
"level": 1
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"title": "5. Preparing Django for Docker",
|
|
239
|
+
"start": 293,
|
|
240
|
+
"end": 350,
|
|
241
|
+
"level": 1
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"title": "6. Volumes and Networks",
|
|
245
|
+
"start": 350,
|
|
246
|
+
"end": 394,
|
|
247
|
+
"level": 1
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"title": "7. Docker Compose",
|
|
251
|
+
"start": 394,
|
|
252
|
+
"end": 529,
|
|
253
|
+
"level": 1
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"title": "8. Registries and Deployment",
|
|
257
|
+
"start": 529,
|
|
258
|
+
"end": 596,
|
|
259
|
+
"level": 1
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
"title": "9. Debugging and Clean Up",
|
|
263
|
+
"start": 596,
|
|
264
|
+
"end": 660,
|
|
265
|
+
"level": 1
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"5": [
|
|
269
|
+
{
|
|
270
|
+
"title": "Document introduction",
|
|
271
|
+
"start": 0,
|
|
272
|
+
"end": 6,
|
|
273
|
+
"level": 1
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"title": "SETUP",
|
|
277
|
+
"start": 6,
|
|
278
|
+
"end": 11,
|
|
279
|
+
"level": 1
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"title": "START A REPOSITORY",
|
|
283
|
+
"start": 11,
|
|
284
|
+
"end": 15,
|
|
285
|
+
"level": 1
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
"title": "DAILY WORK",
|
|
289
|
+
"start": 15,
|
|
290
|
+
"end": 25,
|
|
291
|
+
"level": 1
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"title": "BRANCHES",
|
|
295
|
+
"start": 25,
|
|
296
|
+
"end": 32,
|
|
297
|
+
"level": 1
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
"title": "REMOTES",
|
|
301
|
+
"start": 32,
|
|
302
|
+
"end": 40,
|
|
303
|
+
"level": 1
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"title": "TEMPORARY WORK",
|
|
307
|
+
"start": 40,
|
|
308
|
+
"end": 47,
|
|
309
|
+
"level": 1
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
"title": "UNDO",
|
|
313
|
+
"start": 47,
|
|
314
|
+
"end": 53,
|
|
315
|
+
"level": 1
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
"title": "CONFLICTS",
|
|
319
|
+
"start": 53,
|
|
320
|
+
"end": 60,
|
|
321
|
+
"level": 1
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"title": "TAGS AND IGNORE RULES",
|
|
325
|
+
"start": 60,
|
|
326
|
+
"end": 65,
|
|
327
|
+
"level": 1
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
Binary file
|