epstein-files 1.1.5__py3-none-any.whl → 1.2.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.
@@ -33,7 +33,7 @@ LOG_LEVEL_ENV_VAR = 'LOG_LEVEL'
33
33
  # Augment the standard log highlighter with 'epstein_filename' matcher
34
34
  class LogHighlighter(ReprHighlighter):
35
35
  highlights = ReprHighlighter.highlights + [
36
- *[fr"(?P<{doc_type}>{doc_type}(Cfg)?)" for doc_type in DOC_TYPE_STYLES.keys()],
36
+ *[fr"(?P<{doc_type}>{doc_type}(Cfg|s)?)" for doc_type in DOC_TYPE_STYLES.keys()],
37
37
  "(?P<epstein_filename>" + FILE_NAME_REGEX.pattern + ')',
38
38
  ]
39
39
 
@@ -1,28 +1,31 @@
1
1
  import json
2
+ from os import unlink
3
+ from typing import cast
2
4
 
3
5
  from rich.padding import Padding
4
6
 
5
7
  from epstein_files.documents.document import Document
6
- from epstein_files.documents.email import KRASSNER_RECIPIENTS, Email
8
+ from epstein_files.documents.email import Email
7
9
  from epstein_files.documents.messenger_log import MessengerLog
8
10
  from epstein_files.documents.other_file import FIRST_FEW_LINES, OtherFile
9
11
  from epstein_files.epstein_files import EpsteinFiles, count_by_month
12
+ from epstein_files.person import Person
10
13
  from epstein_files.util.constant import output_files
11
14
  from epstein_files.util.constant.html import *
12
15
  from epstein_files.util.constant.names import *
13
- from epstein_files.util.constant.output_files import JSON_FILES_JSON_PATH, JSON_METADATA_PATH
14
- from epstein_files.util.constant.strings import DEFAULT_NAME_STYLE, TIMESTAMP_DIM, TIMESTAMP_STYLE
15
- from epstein_files.util.data import dict_sets_to_lists, sort_dict
16
+ from epstein_files.util.constant.output_files import EMAILERS_TABLE_PNG_PATH, JSON_FILES_JSON_PATH, JSON_METADATA_PATH
17
+ from epstein_files.util.constant.strings import AUTHOR, TIMESTAMP_STYLE
18
+ from epstein_files.util.data import dict_sets_to_lists, uniquify
16
19
  from epstein_files.util.env import args
17
20
  from epstein_files.util.file_helper import log_file_write
18
- from epstein_files.util.highlighted_group import (JUNK_EMAILERS, QUESTION_MARKS_TXT, get_category_txt_for_name,
19
- get_info_for_name, get_style_for_name, styled_name)
20
- from epstein_files.util.logging import logger
21
+ from epstein_files.util.logging import logger, exit_with_error
21
22
  from epstein_files.util.rich import *
22
23
 
24
+ DEVICE_SIGNATURE_SUBTITLE = f"Email [italic]Sent from \\[DEVICE][/italic] Signature Breakdown"
25
+ DEVICE_SIGNATURE = 'Device Signature'
26
+ DEVICE_SIGNATURE_PADDING = (1, 0)
23
27
  OTHER_INTERESTING_EMAILS_SUBTITLE = 'Other Interesting Emails\n(these emails have been flagged as being of particular interest)'
24
28
  PRINT_COLOR_KEY_EVERY_N_EMAILS = 150
25
- ALT_INFO_STYLE = 'medium_purple4'
26
29
 
27
30
  # Order matters. Default names to print emails for.
28
31
  DEFAULT_EMAILERS = [
@@ -58,20 +61,23 @@ INTERESTING_EMAIL_IDS = [
58
61
  '030630', # 'What happens with zubair's project?'
59
62
  '033178', # 'How is it going with Zubair?'
60
63
  '022396', # Ukraine friend
61
- # '023627', # Michael Wolff article (already printed bc epstein->epstein email)
64
+ '026505', # I know how dirty trump is
65
+ '029679', # Trump's driver was the bag man
66
+ '030781', '026258', '026260', # Bannon cripto coin issues
67
+ '023627', # Michael Wolff article w/Brock
68
+ '032359', # Jabor e-currency
69
+ #'023208', # Extremely long Leon Black email chain
62
70
  ]
63
71
 
64
- INVALID_FOR_EPSTEIN_WEB = JUNK_EMAILERS + KRASSNER_RECIPIENTS + [
65
- 'ACT for America',
66
- 'BS Stern',
67
- INTELLIGENCE_SQUARED,
68
- UNKNOWN,
72
+ INTERESTING_TEXT_IDS = [
73
+ '027275', # "Crypto- Kerry- Qatar -sessions"
74
+ '027165', # melaniee walker crypto health
69
75
  ]
70
76
 
71
77
 
72
78
  def print_email_timeline(epstein_files: EpsteinFiles) -> None:
73
79
  """Print a table of all emails in chronological order."""
74
- emails = Document.sort_by_timestamp([e for e in epstein_files.non_duplicate_emails() if not e.is_junk_mail()])
80
+ emails = Document.sort_by_timestamp([e for e in epstein_files.non_duplicate_emails() if not e.is_mailing_list()])
75
81
  title = f'Table of All {len(emails):,} Non-Junk Emails in Chronological Order (actual emails below)'
76
82
  table = Email.build_emails_table(emails, title=title, show_length=True)
77
83
  console.print(Padding(table, (2, 0)))
@@ -82,30 +88,55 @@ def print_email_timeline(epstein_files: EpsteinFiles) -> None:
82
88
  console.print(email)
83
89
 
84
90
 
91
+ def print_emailers_info(epstein_files: EpsteinFiles) -> None:
92
+ """Print tbe summary table of everyone in the files to an image."""
93
+ print_color_key()
94
+ console.line()
95
+ all_emailers = sorted(epstein_files.emailers(), key=lambda person: person.sort_key())
96
+ console.print(Person.emailer_info_table(all_emailers, show_epstein_total=True))
97
+
98
+ if not args.build:
99
+ logger.warning(f"Not writing .png file because --build is not set")
100
+ return
101
+
102
+ svg_path = f"{EMAILERS_TABLE_PNG_PATH}.svg"
103
+ console.save_svg(svg_path, theme=HTML_TERMINAL_THEME, title="Epstein Emailers")
104
+ log_file_write(svg_path)
105
+ import cairosvg
106
+ cairosvg.svg2png(url=svg_path, write_to=str(EMAILERS_TABLE_PNG_PATH))
107
+ log_file_write(EMAILERS_TABLE_PNG_PATH)
108
+ unlink(svg_path)
109
+
110
+
85
111
  def print_emails_section(epstein_files: EpsteinFiles) -> list[Email]:
86
112
  """Returns emails that were printed (may contain dupes if printed for both author and recipient)."""
87
113
  print_section_header(('Selections from ' if not args.all_emails else '') + 'His Emails')
88
- emailers_to_print: list[str | None]
89
- already_printed_emails: list[Email] = []
114
+ all_emailers = sorted(epstein_files.emailers(), key=lambda person: person.earliest_email_at())
90
115
  num_emails_printed_since_last_color_key = 0
116
+ printed_emails: list[Email] = []
117
+ people_to_print: list[Person]
91
118
 
92
119
  if args.names:
93
- emailers_to_print = args.names
120
+ try:
121
+ people_to_print = epstein_files.person_objs(args.names)
122
+ except Exception as e:
123
+ exit_with_error(str(e))
94
124
  else:
95
125
  if args.all_emails:
96
- emailers_to_print = sorted(epstein_files.all_emailers(), key=lambda e: epstein_files.earliest_email_at(e))
126
+ people_to_print = all_emailers
97
127
  else:
98
- emailers_to_print = DEFAULT_EMAILERS
128
+ people_to_print = epstein_files.person_objs(DEFAULT_EMAILERS)
99
129
 
100
130
  print_other_page_link(epstein_files)
101
- console.line(2)
102
- console.print(_table_of_selected_emailers(emailers_to_print, epstein_files))
103
- console.print(Padding(_all_emailers_table(epstein_files), (2, 0)))
131
+ print_centered(Padding(Person.emailer_info_table(all_emailers, people_to_print), (2, 0, 1, 0)))
132
+
133
+ for person in people_to_print:
134
+ if person.name in epstein_files.uninteresting_emailers() and not args.names:
135
+ continue
104
136
 
105
- for author in emailers_to_print:
106
- author_emails = epstein_files.print_emails_for(author)
107
- already_printed_emails.extend(author_emails)
108
- num_emails_printed_since_last_color_key += len(author_emails)
137
+ printed_person_emails = person.print_emails()
138
+ printed_emails.extend(printed_person_emails)
139
+ num_emails_printed_since_last_color_key += len(printed_person_emails)
109
140
 
110
141
  # Print color key every once in a while
111
142
  if num_emails_printed_since_last_color_key > PRINT_COLOR_KEY_EVERY_N_EMAILS:
@@ -113,26 +144,28 @@ def print_emails_section(epstein_files: EpsteinFiles) -> list[Email]:
113
144
  num_emails_printed_since_last_color_key = 0
114
145
 
115
146
  if args.names:
116
- return already_printed_emails
147
+ return printed_emails
117
148
 
118
149
  # Print other interesting emails
119
- already_printed_ids = [email.file_id for email in already_printed_emails]
120
- extra_emails = [e for e in epstein_files.for_ids(INTERESTING_EMAIL_IDS) if e.file_id not in already_printed_ids]
121
- print_subtitle_panel(OTHER_INTERESTING_EMAILS_SUBTITLE)
122
- console.line()
150
+ printed_email_ids = [email.file_id for email in printed_emails]
151
+ extra_emails = [e for e in epstein_files.for_ids(INTERESTING_EMAIL_IDS) if e.file_id not in printed_email_ids]
123
152
 
124
- for other_email in extra_emails:
125
- console.print(other_email)
153
+ if len(extra_emails) > 0:
154
+ print_subtitle_panel(OTHER_INTERESTING_EMAILS_SUBTITLE)
155
+ console.line()
126
156
 
127
- epstein_files.print_email_device_info()
157
+ for other_email in extra_emails:
158
+ console.print(other_email)
159
+ printed_emails.append(cast(Email, other_email))
128
160
 
129
161
  if args.all_emails:
130
- _verify_all_emails_were_printed(epstein_files, already_printed_emails)
162
+ _verify_all_emails_were_printed(epstein_files, printed_emails)
131
163
 
132
- fwded_articles = [e for e in already_printed_emails if e.config and e.is_fwded_article()]
133
- log_msg = f"Rewrote {len(Email.rewritten_header_ids)} of {len(already_printed_emails)} email headers"
134
- logger.warning(f"{log_msg}, {len(fwded_articles)} of the emails were forwarded articles.")
135
- return already_printed_emails
164
+ _print_email_device_signature_info(epstein_files)
165
+ fwded_articles = [e for e in printed_emails if e.config and e.is_fwded_article()]
166
+ log_msg = f"Rewrote {len(Email.rewritten_header_ids)} of {len(printed_emails)} email headers"
167
+ logger.warning(f" -> {log_msg}, {len(fwded_articles)} of the Emails printed were forwarded articles.")
168
+ return printed_emails
136
169
 
137
170
 
138
171
  def print_json_files(epstein_files: EpsteinFiles):
@@ -165,31 +198,39 @@ def print_json_stats(epstein_files: EpsteinFiles) -> None:
165
198
  console.line(5)
166
199
  console.print(Panel('JSON Stats Dump', expand=True, style='reverse bold'), '\n')
167
200
  print_json(f"MessengerLog Sender Counts", MessengerLog.count_authors(epstein_files.imessage_logs), skip_falsey=True)
168
- print_json(f"Email Author Counts", epstein_files.email_author_counts, skip_falsey=True)
169
- print_json(f"Email Recipient Counts", epstein_files.email_recipient_counts, skip_falsey=True)
201
+ print_json(f"Email Author Counts", epstein_files.email_author_counts(), skip_falsey=True)
202
+ print_json(f"Email Recipient Counts", epstein_files.email_recipient_counts(), skip_falsey=True)
170
203
  print_json("Email signature_substitution_countss", epstein_files.email_signature_substitution_counts(), skip_falsey=True)
171
- print_json("email_author_device_signatures", dict_sets_to_lists(epstein_files.email_authors_to_device_signatures))
172
- print_json("email_sent_from_devices", dict_sets_to_lists(epstein_files.email_device_signatures_to_authors))
173
- print_json("email_unknown_recipient_file_ids", epstein_files.email_unknown_recipient_file_ids())
204
+ print_json("email_author_device_signatures", dict_sets_to_lists(epstein_files.email_authors_to_device_signatures()))
205
+ print_json("email_sent_from_devices", dict_sets_to_lists(epstein_files.email_device_signatures_to_authors()))
206
+ print_json("unknown_recipient_ids", epstein_files.unknown_recipient_ids())
174
207
  print_json("count_by_month", count_by_month(epstein_files.all_documents()))
175
208
 
176
209
 
177
- def print_other_files_section(files: list[OtherFile], epstein_files: EpsteinFiles) -> None:
210
+ def print_other_files_section(epstein_files: EpsteinFiles) -> list[OtherFile]:
178
211
  """Returns the OtherFile objects that were interesting enough to print."""
212
+ if args.uninteresting:
213
+ files = [f for f in epstein_files.other_files if not f.is_interesting()]
214
+ else:
215
+ files = [f for f in epstein_files.other_files if args.all_other_files or f.is_interesting()]
216
+
179
217
  title_pfx = '' if args.all_other_files else 'Selected '
180
- category_table = OtherFile.count_by_category_table(files, title_pfx=title_pfx)
218
+ category_table = OtherFile.summary_table(files, title_pfx=title_pfx)
181
219
  other_files_preview_table = OtherFile.files_preview_table(files, title_pfx=title_pfx)
182
220
  print_section_header(f"{FIRST_FEW_LINES} of {len(files)} {title_pfx}Files That Are Neither Emails Nor Text Messages")
183
221
  print_other_page_link(epstein_files)
184
222
  print_centered(Padding(category_table, (2, 0)))
185
223
  console.print(other_files_preview_table)
224
+ return files
186
225
 
187
226
 
188
- def print_text_messages_section(imessage_logs: list[MessengerLog]) -> None:
227
+ def print_text_messages_section(epstein_files: EpsteinFiles) -> list[MessengerLog]:
189
228
  """Print summary table and stats for text messages."""
229
+ imessage_logs = [log for log in epstein_files.imessage_logs if not args.names or log.author in args.names]
230
+
190
231
  if not imessage_logs:
191
- logger.warning(f"No MessengerLog objects to output...")
192
- return
232
+ logger.warning(f"No MessengerLogs found for {args.names}")
233
+ return imessage_logs
193
234
 
194
235
  print_section_header('All of His Text Messages')
195
236
  print_centered("(conversations are sorted chronologically based on timestamp of first message in the log file)", style='dim')
@@ -203,6 +244,8 @@ def print_text_messages_section(imessage_logs: list[MessengerLog]) -> None:
203
244
  console.print(Padding(log_file))
204
245
  console.line(2)
205
246
 
247
+ return imessage_logs
248
+
206
249
 
207
250
  def write_urls() -> None:
208
251
  """Write _URL style constant variables to URLS_ENV file so bash scripts can load as env vars."""
@@ -226,113 +269,26 @@ def write_urls() -> None:
226
269
  logger.warning(f"Wrote {len(url_vars)} URL variables to '{URLS_ENV}'\n")
227
270
 
228
271
 
229
- def _all_emailers_table(epstein_files: EpsteinFiles) -> Table:
230
- attributed_emails = [e for e in epstein_files.non_duplicate_emails() if e.author]
231
- footer = f"(identified {len(epstein_files.email_author_counts)} authors of {len(attributed_emails):,}"
232
- footer = f"{footer} out of {len(epstein_files.non_duplicate_emails()):,} emails)"
233
- counts_table = build_table("Everyone Who Sent or Received an Email in the Files", caption=footer)
234
-
235
- add_cols_to_table(counts_table, [
236
- 'Name',
237
- {'name': 'Count', 'justify': 'right', 'style': 'bold bright_white'},
238
- {'name': 'Sent', 'justify': 'right', 'style': 'gray74'},
239
- {'name': 'Recv', 'justify': 'right', 'style': 'gray74'},
240
- {'name': 'First', 'style': TIMESTAMP_STYLE},
241
- {'name': 'Last', 'style': LAST_TIMESTAMP_STYLE},
242
- {'name': 'Days', 'justify': 'right', 'style': 'dim'},
243
- JMAIL,
244
- EPSTEIN_MEDIA,
245
- EPSTEIN_WEB,
246
- 'Twitter',
247
- ])
248
-
249
- emailer_counts = {
250
- emailer: epstein_files.email_author_counts[emailer] + epstein_files.email_recipient_counts[emailer]
251
- for emailer in epstein_files.all_emailers(include_useless=True)
252
- }
253
-
254
- for name, count in sort_dict(emailer_counts):
255
- style = get_style_for_name(name, default_style=DEFAULT_NAME_STYLE)
256
- emails = epstein_files.emails_for(name)
257
-
258
- counts_table.add_row(
259
- Text.from_markup(link_markup(epsteinify_name_url(name or UNKNOWN), name or UNKNOWN, style)),
260
- f"{count:,}",
261
- str(epstein_files.email_author_counts[name]),
262
- str(epstein_files.email_recipient_counts[name]),
263
- emails[0].date_str(),
264
- emails[-1].date_str(),
265
- f"{epstein_files.email_conversation_length_in_days(name)}",
266
- link_text_obj(search_jmail_url(name), JMAIL) if name else '',
267
- link_text_obj(epstein_media_person_url(name), EPSTEIN_MEDIA) if _is_ok_for_epstein_web(name) else '',
268
- link_text_obj(epstein_web_person_url(name), EPSTEIN_WEB) if _is_ok_for_epstein_web(name) else '',
269
- link_text_obj(search_twitter_url(name), 'search X') if name else '',
270
- )
271
-
272
- return counts_table
273
-
274
-
275
- def _is_ok_for_epstein_web(name: str | None) -> bool:
276
- """Return True if it's likely that EpsteinWeb has a page for this name."""
277
- if name is None or ' ' not in name:
278
- return False
279
- elif '@' in name or '/' in name or '??' in name:
280
- return False
281
- elif name in INVALID_FOR_EPSTEIN_WEB:
282
- return False
283
-
284
- return True
285
-
286
-
287
- def _table_of_selected_emailers(_list: list[str | None], epstein_files: EpsteinFiles) -> Table:
288
- """Add the first emailed_at timestamp for each emailer if 'epstein_files' provided."""
289
- header_pfx = '' if args.all_emails else 'Selected '
290
- table = build_table(f'{header_pfx}Email Conversations Grouped by Counterparty Will Appear in this Order')
291
- table.add_column('Start Date')
292
- table.add_column('Name', max_width=25, no_wrap=True)
293
- table.add_column('Category', justify='center', style='dim italic')
294
- table.add_column('Num', justify='right', style='wheat4')
295
- table.add_column('Info', style='white italic')
296
- current_year = 1990
297
- current_year_month = current_year * 12
298
- grey_idx = 0
299
-
300
- for i, name in enumerate(_list):
301
- earliest_email_date = (epstein_files.earliest_email_at(name)).date()
302
- year_months = (earliest_email_date.year * 12) + earliest_email_date.month
303
-
304
- # Color year rollovers more brightly
305
- if current_year != earliest_email_date.year:
306
- grey_idx = 0
307
- elif current_year_month != year_months:
308
- grey_idx = ((current_year_month - 1) % 12) + 1
309
-
310
- current_year_month = year_months
311
- current_year = earliest_email_date.year
312
- category = get_category_txt_for_name(name)
313
- info = get_info_for_name(name)
314
- style = get_style_for_name(name, default_style='none')
315
-
316
- if name == JEFFREY_EPSTEIN:
317
- info = Text('(emails sent by Epstein to himself that would not otherwise be printed)', style=ALT_INFO_STYLE)
318
- if category and category.plain == 'paula': # TODO: hacky
319
- category = None
320
- elif category and info:
321
- info = info.removeprefix(f"{category.plain}, ").removeprefix(category.plain)
322
- elif not name:
323
- info = Text('(emails whose author or recipient could not be determined)', style=ALT_INFO_STYLE)
324
- elif style == 'none' and '@' not in name and not (category or info):
325
- info = QUESTION_MARKS_TXT
326
-
327
- table.add_row(
328
- Text(str(earliest_email_date), style=f"grey{GREY_NUMBERS[grey_idx]}"),
329
- styled_name(name, default_style='dim'),
330
- category,
331
- f"{len(epstein_files.emails_for(name)):,}",
332
- info or '',
333
- )
334
-
335
- return table
272
+ def _print_email_device_signature_info(epstein_files: EpsteinFiles) -> None:
273
+ print_subtitle_panel(DEVICE_SIGNATURE_SUBTITLE)
274
+ console.print(_signature_table(epstein_files.email_device_signatures_to_authors(), (DEVICE_SIGNATURE, AUTHOR), ', '))
275
+ console.print(_signature_table(epstein_files.email_authors_to_device_signatures(), (AUTHOR, DEVICE_SIGNATURE)))
276
+
277
+
278
+ def _signature_table(keyed_sets: dict[str, set[str]], cols: tuple[str, str], join_char: str = '\n') -> Padding:
279
+ """Build table for who signed emails with 'Sent from my iPhone' etc."""
280
+ title = 'Email Signatures Used By Authors' if cols[0] == AUTHOR else 'Authors Seen Using Email Signatures'
281
+ table = build_table(title, header_style="bold reverse", show_lines=True)
282
+
283
+ for i, col in enumerate(cols):
284
+ table.add_column(col.title() + ('s' if i == 1 else ''))
285
+
286
+ new_dict = dict_sets_to_lists(keyed_sets)
287
+
288
+ for k in sorted(new_dict.keys()):
289
+ table.add_row(highlighter(k or UNKNOWN), highlighter(join_char.join(sorted(new_dict[k]))))
290
+
291
+ return Padding(table, DEVICE_SIGNATURE_PADDING)
336
292
 
337
293
 
338
294
  def _verify_all_emails_were_printed(epstein_files: EpsteinFiles, already_printed_emails: list[Email]) -> None:
@@ -18,7 +18,7 @@ from epstein_files.util.constant.names import UNKNOWN
18
18
  from epstein_files.util.constant.strings import DEFAULT, EMAIL, NA, TEXT_MESSAGE
19
19
  from epstein_files.util.constant.urls import *
20
20
  from epstein_files.util.constants import HEADER_ABBREVIATIONS
21
- from epstein_files.util.data import json_safe, without_falsey
21
+ from epstein_files.util.data import json_safe
22
22
  from epstein_files.util.env import args
23
23
  from epstein_files.util.file_helper import log_file_write
24
24
  from epstein_files.util.highlighted_group import ALL_HIGHLIGHTS, HIGHLIGHTED_NAMES, EpsteinHighlighter
@@ -26,11 +26,11 @@ from epstein_files.util.logging import logger
26
26
 
27
27
  TITLE_WIDTH = 50
28
28
  SUBTITLE_WIDTH = 110
29
- MIN_AUTHOR_PANEL_WIDTH = 80
30
29
  NUM_COLOR_KEY_COLS = 4
31
30
  NA_TXT = Text(NA, style='dim')
32
31
  SUBTITLE_PADDING = (2, 0, 1, 0)
33
32
  GREY_NUMBERS = [58, 39, 39, 35, 30, 27, 23, 23, 19, 19, 15, 15, 15]
33
+ VALID_GREYS = [0, 3, 7, 11, 15, 19, 23, 27, 30, 35, 37, 39, 42, 46, 50, 53, 54, 58, 62, 63, 66, 69, 70, 74, 78, 82, 84, 85, 89, 93]
34
34
 
35
35
  INFO_STYLE = 'white dim italic'
36
36
  KEY_STYLE = 'honeydew2 bold'
@@ -70,7 +70,7 @@ CONSOLE_ARGS = {
70
70
  'color_system': '256',
71
71
  'highlighter': EpsteinHighlighter(),
72
72
  'record': args.build,
73
- 'safe_box': False,
73
+ 'safe_box': True,
74
74
  'theme': Theme(THEME_STYLES),
75
75
  'width': args.width,
76
76
  }
@@ -83,21 +83,21 @@ console = Console(**CONSOLE_ARGS)
83
83
  highlighter = CONSOLE_ARGS['highlighter']
84
84
 
85
85
 
86
- def add_cols_to_table(table: Table, col_names: list[str | dict]) -> None:
86
+ def add_cols_to_table(table: Table, cols: list[str | dict], justify: str = 'center') -> None:
87
87
  """Left most col will be left justified, rest are center justified."""
88
- for i, col in enumerate(col_names):
89
- justify='left' if i == 0 else 'center'
88
+ for i, col in enumerate(cols):
89
+ col_justify = 'left' if i == 0 else justify
90
90
 
91
91
  if isinstance(col, dict):
92
92
  col_name = col['name']
93
- kwargs = deepcopy(col)
94
- kwargs['justify'] = kwargs.get('justify', justify)
95
- del kwargs['name']
93
+ col_kwargs = deepcopy(col)
94
+ col_kwargs['justify'] = col_kwargs.get('justify', col_justify)
95
+ del col_kwargs['name']
96
96
  else:
97
97
  col_name = col
98
- kwargs = {'justify': justify}
98
+ col_kwargs = {'justify': col_justify}
99
99
 
100
- table.add_column(col_name, **kwargs)
100
+ table.add_column(col_name, **col_kwargs)
101
101
 
102
102
 
103
103
  def build_highlighter(pattern: str) -> EpsteinHighlighter:
@@ -108,7 +108,7 @@ def build_highlighter(pattern: str) -> EpsteinHighlighter:
108
108
  return TempHighlighter()
109
109
 
110
110
 
111
- def build_table(title: str | None, cols: list[str | dict] | None = None, **kwargs) -> Table:
111
+ def build_table(title: str | Text | None, cols: list[str | dict] | None = None, **kwargs) -> Table:
112
112
  table = Table(title=title, **{**DEFAULT_TABLE_KWARGS, **kwargs})
113
113
 
114
114
  if cols:
@@ -149,19 +149,6 @@ def parenthesize(msg: str | Text, style: str = '') -> Text:
149
149
  return Text('(', style=style).append(txt).append(')')
150
150
 
151
151
 
152
- def print_author_panel(msg: str, footer: str | None, style: str | None) -> None:
153
- """Print a panel with the name of an emailer and a few tidbits of information about them."""
154
- style = 'white' if (not style or style == DEFAULT) else style
155
- panel_style = f"black on {style} bold"
156
- width = max(MIN_AUTHOR_PANEL_WIDTH, len(msg) + 4, len(footer or '') + 8)
157
- elements: list[RenderableType] = [Panel(Text(msg, justify='center'), width=width, style=panel_style)]
158
-
159
- if footer:
160
- elements.append(Text(f"({footer})", justify='center', style=f"{style} italic"))
161
-
162
- print_centered(Padding(Group(*elements), (2, 0, 1, 0)))
163
-
164
-
165
152
  def print_centered(obj: RenderableType, style: str = '') -> None:
166
153
  console.print(Align.center(obj), style=style)
167
154
 
@@ -200,7 +187,7 @@ def print_title_page_header() -> None:
200
187
  Text.from_markup(link_markup(other_site_url(), other_site_msg, f"{OTHER_SITE_LINK_STYLE} bold")),
201
188
  link_text_obj(WORD_COUNT_URL, 'most frequently used words in the emails and texts', AUX_SITE_LINK_STYLE),
202
189
  link_text_obj(JSON_METADATA_URL, 'author attribution explanations', AUX_SITE_LINK_STYLE),
203
- link_text_obj(WORD_COUNT_URL, "epstein's json files", AUX_SITE_LINK_STYLE),
190
+ link_text_obj(JSON_FILES_URL, "epstein's json files", AUX_SITE_LINK_STYLE),
204
191
  ]
205
192
 
206
193
  for link in links:
@@ -212,7 +199,8 @@ def print_title_page_tables(epstein_files: 'EpsteinFiles') -> None:
212
199
  _print_external_links()
213
200
  console.line()
214
201
  _print_abbreviations_table()
215
- epstein_files.print_files_summary()
202
+ print_centered(epstein_files.overview_table())
203
+ console.line()
216
204
  print_color_key()
217
205
  print_centered(f"if you think there's an attribution error or can deanonymize an {UNKNOWN} contact {CRYPTADAMUS_TWITTER}", 'grey46')
218
206
  print_centered('note this site is based on the OCR text provided by Congress which is not always the greatest', 'grey23')
@@ -234,13 +222,11 @@ def print_json(label: str, obj: object, skip_falsey: bool = False) -> None:
234
222
 
235
223
 
236
224
  def print_other_page_link(epstein_files: 'EpsteinFiles') -> None:
237
- markup_msg = link_markup(other_site_url(), 'the other page', style='light_slate_grey bold')
238
-
239
225
  if other_site_type() == EMAIL:
240
- txt = Text.from_markup(markup_msg).append(f' is uncurated and has all {len(epstein_files.emails):,} emails')
226
+ txt = THE_OTHER_PAGE_TXT + Text(f' is uncurated and has all {len(epstein_files.emails):,} emails')
241
227
  txt.append(f" and {len(epstein_files.other_files)} unclassifiable files")
242
228
  else:
243
- txt = Text.from_markup(markup_msg).append(f' displays a limited collection of emails and')
229
+ txt = THE_OTHER_PAGE_TXT + (f' displays a limited collection of emails and')
244
230
  txt.append(" unclassifiable files of particular interest")
245
231
 
246
232
  print_centered(parenthesize(txt), style=OTHER_PAGE_MSG_STYLE)
@@ -267,7 +253,7 @@ def print_subtitle_panel(msg: str, style: str = 'black on white') -> None:
267
253
  def print_section_header(msg: str, style: str = SECTION_HEADER_STYLE, is_centered: bool = False) -> None:
268
254
  panel = Panel(Text(msg, justify='center'), expand=True, padding=(1, 1), style=style)
269
255
  panel = Align.center(panel) if is_centered else panel
270
- console.print(Padding(panel, (3, 0, 1, 0)))
256
+ console.print(Padding(panel, (3, 5, 1, 5)))
271
257
 
272
258
 
273
259
  def print_starred_header(msg: str, num_stars: int = 7, num_spaces: int = 2, style: str = TITLE_STYLE) -> None:
@@ -311,7 +297,7 @@ def write_html(output_path: Path | None) -> None:
311
297
  def _print_abbreviations_table() -> None:
312
298
  table = build_table(title="Abbreviations Used Frequently In These Conversations", show_header=False)
313
299
  table.add_column("Abbreviation", justify="center", style='bold')
314
- table.add_column("Translation", style="white", justify="center")
300
+ table.add_column("Translation", justify="center", min_width=62, style="white")
315
301
 
316
302
  for k, v in HEADER_ABBREVIATIONS.items():
317
303
  table.add_row(highlighter(k), v)
@@ -326,8 +312,7 @@ def _print_external_links() -> None:
326
312
  raw_docs_link = join_texts([link_text_obj(RAW_OVERSIGHT_DOCS_GOOGLE_DRIVE_URL, 'raw files', style=f"{ARCHIVE_LINK_COLOR} dim")], encloser='()')
327
313
  print_centered(join_texts([presser_link, raw_docs_link]))
328
314
  print_centered(link_markup(JMAIL_URL, JMAIL) + " (read His Emails via Gmail interface)")
329
- print_centered(link_markup(COFFEEZILLA_ARCHIVE_URL, 'Archive Of Epstein Materials') + " (Coffeezilla)")
330
- print_centered(link_markup(COURIER_NEWSROOM_ARCHIVE_URL, 'Searchable Archive') + " (Courier Newsroom)")
315
+ print_centered(link_markup(EPSTEIN_DOCS_URL) + " (searchable archive)")
331
316
  print_centered(link_markup(EPSTEINIFY_URL) + " (raw document images)")
332
317
  print_centered(link_markup(EPSTEIN_WEB_URL) + " (character summaries)")
333
318
  print_centered(link_markup(EPSTEIN_MEDIA_URL) + " (raw document images)")
@@ -1,5 +1,6 @@
1
1
  import time
2
2
  from dataclasses import dataclass, field
3
+ from typing import Type
3
4
 
4
5
  from epstein_files.util.logging import logger
5
6
 
@@ -10,6 +11,19 @@ class Timer:
10
11
  checkpoint_at: float = field(default_factory=lambda: time.perf_counter())
11
12
  decimals: int = 2
12
13
 
14
+ def log_section_complete(self, label: str, all_docs: list, printed_docs: list) -> None:
15
+ num_skipped = len(all_docs) - len(printed_docs)
16
+ prefix = suffix = ''
17
+
18
+ if num_skipped == 0:
19
+ prefix = 'all '
20
+ elif num_skipped < 0:
21
+ suffix = f"(at least {num_skipped} {label}s printed more than once)"
22
+ else:
23
+ suffix = f"(skipped {num_skipped})"
24
+
25
+ self.print_at_checkpoint(f"Printed {prefix}{len(printed_docs)} {label}s {suffix}".strip())
26
+
13
27
  def print_at_checkpoint(self, msg: str) -> None:
14
28
  logger.warning(f"{msg} in {self.seconds_since_checkpoint_str()}...")
15
29
  self.checkpoint_at = time.perf_counter()
@@ -197,7 +197,7 @@ def write_word_counts_html() -> None:
197
197
  email_subjects: set[str] = set()
198
198
  word_count = WordCount()
199
199
  # Remove dupes, junk mail, and fwded articles from emails
200
- emails = [e for e in epstein_files.non_duplicate_emails() if not (e.is_junk_mail() or e.is_fwded_article())]
200
+ emails = [e for e in epstein_files.non_duplicate_emails() if not (e.is_mailing_list() or e.is_fwded_article())]
201
201
 
202
202
  for email in emails:
203
203
  if args.names and email.author not in args.names:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: epstein-files
3
- Version: 1.1.5
3
+ Version: 1.2.1
4
4
  Summary: Tools for working with the Jeffrey Epstein documents released in November 2025.
5
5
  Home-page: https://michelcrypt4d4mus.github.io/epstein_text_messages/
6
6
  License: GPL-3.0-or-later
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Dist: cairosvg (>=2.8.2,<3.0.0)
20
21
  Requires-Dist: datefinder (>=0.7.3,<0.8.0)
21
22
  Requires-Dist: inflection (>=0.5.1,<0.6.0)
22
23
  Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
@@ -31,7 +32,7 @@ Project-URL: TextMessages, https://michelcrypt4d4mus.github.io/epstein_text_mess
31
32
  Project-URL: WordCounts, https://michelcrypt4d4mus.github.io/epstein_text_messages/communication_word_count_epstein_files_nov_2025.html
32
33
  Description-Content-Type: text/markdown
33
34
 
34
- # I Made Epstein's Text Messages Great Again
35
+ # Color Highlighted Epstein Emails and Text Messages
35
36
 
36
37
  ![joi](https://github.com/michelcrypt4d4mus/epstein_text_messages/raw/master/docs/joi_ito_gavin_is_clever_epstein_funds_bitcoin_dev_team.png)
37
38
 
@@ -119,3 +120,6 @@ for file in epstein_files.other_files:
119
120
  do_stuff(file)
120
121
  ```
121
122
 
123
+ # Everyone Who Sent or Received an Email in the November Document Dump
124
+ ![emails](https://github.com/michelcrypt4d4mus/epstein_text_messages/raw/master/docs/emailers_info_table.png)
125
+
@@ -0,0 +1,34 @@
1
+ epstein_files/__init__.py,sha256=zhFnPc-TFQLJIO0qnXJzDM1Bf_u8nv308GDKAvn405g,4831
2
+ epstein_files/documents/communication.py,sha256=QiCZ35R2ttlqcdovm5LBdGqgoj4xCcpl9ANqwlA9XGU,1752
3
+ epstein_files/documents/document.py,sha256=YwWHaiccKpYiS4UJAiGzDviyYV2hzdeee_vdX4ybhOo,19519
4
+ epstein_files/documents/email.py,sha256=xc2cv6KkMfKhNLMfqW6rmDDXn1ozRChao1i98-0byI8,42297
5
+ epstein_files/documents/emails/email_header.py,sha256=3UD2pXMS9bRsFP4L5RSP2tSjI8OR6lq6gPKKao0DYLY,7739
6
+ epstein_files/documents/imessage/text_message.py,sha256=IexwwVeF14FZqD7IuK47bIHYam07ZcD3rxheVNULNkc,3394
7
+ epstein_files/documents/json_file.py,sha256=WcZW5NNqA67rHTdopbOGtup00muNaLlvrNgKb-K4zO8,1504
8
+ epstein_files/documents/messenger_log.py,sha256=1bv62WoQMKR3gYDrK9W3Xm7cqLbKrkRBV7NTFL2cexE,7349
9
+ epstein_files/documents/other_file.py,sha256=xZSILlnYlX2QIYCQCm6WNHvNqhF5do6AmxT7qxw-yog,9417
10
+ epstein_files/epstein_files.py,sha256=D2yMCm1IM5Skojzvvlod6yTGI_nUtNQmwmX6dzv2Egs,14057
11
+ epstein_files/person.py,sha256=pESfBu2VvsLtrA86V8_D2BHKtb4m0F4xCf7tIrwyp6c,13579
12
+ epstein_files/util/constant/common_words.py,sha256=C1JERPnOGHV2UMC71aEue1i9QTQog-RfT3IzdcYQOYQ,3702
13
+ epstein_files/util/constant/html.py,sha256=MFooFV8KfFBCm9hL1u6A3hi_u37i7lL6UKAYoKQj3PI,1505
14
+ epstein_files/util/constant/names.py,sha256=pkS1BWBxpxveHRrL1Z4muaXcK2ICIwM7RFGCsCf7Qls,11267
15
+ epstein_files/util/constant/output_files.py,sha256=gUZJ4mNoeJy3qTYWr_jhSmQI-_uV_jdLR0YCiaQd_Qg,1982
16
+ epstein_files/util/constant/strings.py,sha256=z_vwWxWU-_85rDTbVyo8pbi-5keBYmYnbh758n2LebU,1989
17
+ epstein_files/util/constant/urls.py,sha256=UvhQP6sWqZaNnkqXFIn44oVmYLWFA-A0FGpo40Ds-MM,5651
18
+ epstein_files/util/constants.py,sha256=VdOOp1azsaFgeEEO9UMS8hx5NXNP4pozfyH2IA6kW9E,121794
19
+ epstein_files/util/data.py,sha256=0kCGgs3Cscbn1Q9G82bSB51CKnQpz1iUzMJoBBM34IE,2964
20
+ epstein_files/util/doc_cfg.py,sha256=gIBAoeheQYqqrCtVRU2ytYu799LyCiPxPyHDgWUad3c,9471
21
+ epstein_files/util/env.py,sha256=bSf2PKkOukN7LA8YdrWQS7XTRY0pLTNXo8OiQ1EKJDE,6722
22
+ epstein_files/util/file_helper.py,sha256=MpG1hI7DGs05fV9KSVb_ltc98DC8tv1E_TTo5X_E7Js,3010
23
+ epstein_files/util/highlighted_group.py,sha256=8uuQAHIBk7c5VYzg2xqfHsR-TGKfX7f1qdwB74UfTfI,56719
24
+ epstein_files/util/logging.py,sha256=pSLHGrUnPQGFMGYNIOTbZbCyy5dDOCbNrbNz22wIcZI,2099
25
+ epstein_files/util/output.py,sha256=pqTu6MApTiRKc_Q0mZzKD73UAXnAbbA5zWzGijAH4OU,12681
26
+ epstein_files/util/rich.py,sha256=z9wbvNQbOJqNWm7IDBNJFgRzMP2X5CTmOgwrOkuSEJk,13639
27
+ epstein_files/util/search_result.py,sha256=1fxe0KPBQXBk4dLfu6m0QXIzYfZCzvaSkWqvghJGzxY,567
28
+ epstein_files/util/timer.py,sha256=GZHefXiPA-prK-1szC1cebl44Y-i4Wd3K8zedFpcggg,1373
29
+ epstein_files/util/word_count.py,sha256=rs-bsSMnGG1BnYUmxYuBLZ8X0-3-jJdaB03v1jOIq4g,9202
30
+ epstein_files-1.2.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
31
+ epstein_files-1.2.1.dist-info/METADATA,sha256=_NTxSxgHoHb1uPeRlLqgxBl_9wJnyMsY2_3icXgZTDw,6222
32
+ epstein_files-1.2.1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
33
+ epstein_files-1.2.1.dist-info/entry_points.txt,sha256=5qYgwAXpxegeAicD_rzda_trDRnUC51F5UVDpcZ7j6Q,240
34
+ epstein_files-1.2.1.dist-info/RECORD,,