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.
- epstein_files/__init__.py +12 -21
- epstein_files/documents/communication.py +0 -3
- epstein_files/documents/document.py +68 -21
- epstein_files/documents/email.py +54 -70
- epstein_files/documents/emails/email_header.py +14 -4
- epstein_files/documents/imessage/text_message.py +5 -4
- epstein_files/documents/messenger_log.py +7 -7
- epstein_files/documents/other_file.py +16 -34
- epstein_files/epstein_files.py +133 -141
- epstein_files/person.py +324 -0
- epstein_files/util/constant/names.py +46 -15
- epstein_files/util/constant/output_files.py +1 -0
- epstein_files/util/constant/strings.py +3 -3
- epstein_files/util/constant/urls.py +15 -2
- epstein_files/util/constants.py +75 -21
- epstein_files/util/data.py +1 -20
- epstein_files/util/doc_cfg.py +27 -17
- epstein_files/util/env.py +5 -3
- epstein_files/util/highlighted_group.py +248 -203
- epstein_files/util/logging.py +1 -1
- epstein_files/util/output.py +113 -157
- epstein_files/util/rich.py +20 -35
- epstein_files/util/timer.py +14 -0
- epstein_files/util/word_count.py +1 -1
- {epstein_files-1.1.5.dist-info → epstein_files-1.2.1.dist-info}/METADATA +6 -2
- epstein_files-1.2.1.dist-info/RECORD +34 -0
- epstein_files-1.1.5.dist-info/RECORD +0 -33
- {epstein_files-1.1.5.dist-info → epstein_files-1.2.1.dist-info}/LICENSE +0 -0
- {epstein_files-1.1.5.dist-info → epstein_files-1.2.1.dist-info}/WHEEL +0 -0
- {epstein_files-1.1.5.dist-info → epstein_files-1.2.1.dist-info}/entry_points.txt +0 -0
epstein_files/person.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime, date
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from rich.console import Group, RenderableType
|
|
6
|
+
from rich.padding import Padding
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
from epstein_files.documents.document import Document
|
|
12
|
+
from epstein_files.documents.email import MAILING_LISTS, JUNK_EMAILERS, Email
|
|
13
|
+
from epstein_files.documents.messenger_log import MessengerLog
|
|
14
|
+
from epstein_files.documents.other_file import OtherFile
|
|
15
|
+
from epstein_files.util.constant.strings import *
|
|
16
|
+
from epstein_files.util.constant.urls import *
|
|
17
|
+
from epstein_files.util.constants import *
|
|
18
|
+
from epstein_files.util.data import days_between, flatten, without_falsey
|
|
19
|
+
from epstein_files.util.env import args
|
|
20
|
+
from epstein_files.util.highlighted_group import (QUESTION_MARKS_TXT, HighlightedNames,
|
|
21
|
+
get_highlight_group_for_name, get_style_for_name, styled_category, styled_name)
|
|
22
|
+
from epstein_files.util.rich import GREY_NUMBERS, TABLE_TITLE_STYLE, build_table, console, join_texts, print_centered
|
|
23
|
+
|
|
24
|
+
ALT_INFO_STYLE = 'medium_purple4'
|
|
25
|
+
CC = 'cc:'
|
|
26
|
+
MIN_AUTHOR_PANEL_WIDTH = 80
|
|
27
|
+
EMAILER_INFO_TITLE = 'Email Conversations Will Appear'
|
|
28
|
+
UNINTERESTING_CC_INFO = "cc: or bcc: recipient only"
|
|
29
|
+
UNINTERESTING_CC_INFO_NO_CONTACT = f"{UNINTERESTING_CC_INFO}, no direct contact with Epstein"
|
|
30
|
+
|
|
31
|
+
INVALID_FOR_EPSTEIN_WEB = JUNK_EMAILERS + MAILING_LISTS + [
|
|
32
|
+
'ACT for America',
|
|
33
|
+
'BS Stern',
|
|
34
|
+
UNKNOWN,
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(kw_only=True)
|
|
39
|
+
class Person:
|
|
40
|
+
"""Collection of data about someone texting or emailing Epstein."""
|
|
41
|
+
name: Name
|
|
42
|
+
emails: list[Email] = field(default_factory=list)
|
|
43
|
+
imessage_logs: list[MessengerLog] = field(default_factory=list)
|
|
44
|
+
other_files: list[OtherFile] = field(default_factory=list)
|
|
45
|
+
is_uninteresting_cc: bool = False
|
|
46
|
+
|
|
47
|
+
def __post_init__(self):
|
|
48
|
+
self.emails = Document.sort_by_timestamp(self.emails)
|
|
49
|
+
self.imessage_logs = Document.sort_by_timestamp(self.imessage_logs)
|
|
50
|
+
|
|
51
|
+
def category(self) -> str | None:
|
|
52
|
+
highlight_group = self.highlight_group()
|
|
53
|
+
|
|
54
|
+
if highlight_group and isinstance(highlight_group, HighlightedNames):
|
|
55
|
+
category = highlight_group.category or highlight_group.label
|
|
56
|
+
|
|
57
|
+
if category != self.name and category != 'paula': # TODO: this sucks
|
|
58
|
+
return category
|
|
59
|
+
|
|
60
|
+
def category_txt(self) -> Text | None:
|
|
61
|
+
if self.name is None:
|
|
62
|
+
return None
|
|
63
|
+
elif self.category():
|
|
64
|
+
return styled_category(self.category())
|
|
65
|
+
elif self.is_a_mystery() or self.is_uninteresting_cc:
|
|
66
|
+
return QUESTION_MARKS_TXT
|
|
67
|
+
|
|
68
|
+
def email_conversation_length_in_days(self) -> int:
|
|
69
|
+
return days_between(self.emails[0].timestamp, self.emails[-1].timestamp)
|
|
70
|
+
|
|
71
|
+
def earliest_email_at(self) -> datetime:
|
|
72
|
+
return self.emails[0].timestamp
|
|
73
|
+
|
|
74
|
+
def earliest_email_date(self) -> date:
|
|
75
|
+
return self.earliest_email_at().date()
|
|
76
|
+
|
|
77
|
+
def last_email_at(self) -> datetime:
|
|
78
|
+
return self.emails[-1].timestamp
|
|
79
|
+
|
|
80
|
+
def last_email_date(self) -> date:
|
|
81
|
+
return self.last_email_at().date()
|
|
82
|
+
|
|
83
|
+
def emails_by(self) -> list[Email]:
|
|
84
|
+
return [e for e in self.emails if self.name == e.author]
|
|
85
|
+
|
|
86
|
+
def emails_to(self) -> list[Email]:
|
|
87
|
+
return [
|
|
88
|
+
e for e in self.emails
|
|
89
|
+
if self.name in e.recipients or (self.name is None and len(e.recipients) == 0)
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def external_link(self, site: ExternalSite = EPSTEINIFY) -> str:
|
|
93
|
+
return PERSON_LINK_BUILDERS[site](self.name_str())
|
|
94
|
+
|
|
95
|
+
def external_link_txt(self, site: ExternalSite = EPSTEINIFY, link_str: str | None = None) -> Text:
|
|
96
|
+
if self.name is None:
|
|
97
|
+
return Text('')
|
|
98
|
+
|
|
99
|
+
return link_text_obj(self.external_link(site), link_str or site, style=self.style())
|
|
100
|
+
|
|
101
|
+
def external_links_line(self) -> Text:
|
|
102
|
+
links = [self.external_link_txt(site) for site in PERSON_LINK_BUILDERS]
|
|
103
|
+
return Text('', justify='center', style='dim').append(join_texts(links, join=' / ')) #, encloser='()'))#, encloser='‹›'))
|
|
104
|
+
|
|
105
|
+
def has_any_epstein_emails(self) -> bool:
|
|
106
|
+
contacts = [e.author for e in self.emails] + flatten([e.recipients for e in self.emails])
|
|
107
|
+
return JEFFREY_EPSTEIN in contacts
|
|
108
|
+
|
|
109
|
+
def highlight_group(self) -> HighlightedNames | None:
|
|
110
|
+
return get_highlight_group_for_name(self.name)
|
|
111
|
+
|
|
112
|
+
def info_panel(self) -> Padding:
|
|
113
|
+
"""Print a panel with the name of an emailer and a few tidbits of information about them."""
|
|
114
|
+
style = 'white' if (not self.style() or self.style() == DEFAULT) else self.style()
|
|
115
|
+
panel_style = f"black on {style} bold"
|
|
116
|
+
|
|
117
|
+
if self.name == JEFFREY_EPSTEIN:
|
|
118
|
+
email_count = len(self._printable_emails())
|
|
119
|
+
title_suffix = f"sent by {JEFFREY_EPSTEIN} to himself"
|
|
120
|
+
else:
|
|
121
|
+
email_count = len(self.unique_emails())
|
|
122
|
+
num_days = self.email_conversation_length_in_days()
|
|
123
|
+
title_suffix = f"to/from {self.name_str()} starting {self.earliest_email_date()} covering {num_days:,} days"
|
|
124
|
+
|
|
125
|
+
title = f"Found {email_count} emails {title_suffix}"
|
|
126
|
+
width = max(MIN_AUTHOR_PANEL_WIDTH, len(title) + 4, len(self.info_with_category()) + 8)
|
|
127
|
+
panel = Panel(Text(title, justify='center'), width=width, style=panel_style)
|
|
128
|
+
elements: list[RenderableType] = [panel]
|
|
129
|
+
|
|
130
|
+
if self.info_with_category():
|
|
131
|
+
elements.append(Text(f"({self.info_with_category()})", justify='center', style=f"{style} italic"))
|
|
132
|
+
|
|
133
|
+
return Padding(Group(*elements), (2, 0, 1, 0))
|
|
134
|
+
|
|
135
|
+
def info_str(self) -> str | None:
|
|
136
|
+
highlight_group = self.highlight_group()
|
|
137
|
+
|
|
138
|
+
if highlight_group and isinstance(highlight_group, HighlightedNames) and self.name:
|
|
139
|
+
return highlight_group.info_for(self.name)
|
|
140
|
+
elif self.is_uninteresting_cc:
|
|
141
|
+
if self.has_any_epstein_emails():
|
|
142
|
+
return UNINTERESTING_CC_INFO
|
|
143
|
+
else:
|
|
144
|
+
return UNINTERESTING_CC_INFO_NO_CONTACT
|
|
145
|
+
|
|
146
|
+
def info_with_category(self) -> str:
|
|
147
|
+
return ', '.join(without_falsey([self.category(), self.info_str()]))
|
|
148
|
+
|
|
149
|
+
def info_txt(self) -> Text | None:
|
|
150
|
+
if self.name == JEFFREY_EPSTEIN:
|
|
151
|
+
return Text('(emails sent by Epstein to himself are here)', style=ALT_INFO_STYLE)
|
|
152
|
+
elif self.name is None:
|
|
153
|
+
return Text('(emails whose author or recipient could not be determined)', style=ALT_INFO_STYLE)
|
|
154
|
+
elif self.category() == JUNK:
|
|
155
|
+
return Text(f"({JUNK} mail)", style='tan dim')
|
|
156
|
+
elif self.is_uninteresting_cc and (self.info_str() or '').startswith(UNINTERESTING_CC_INFO):
|
|
157
|
+
if self.info_str() == UNINTERESTING_CC_INFO:
|
|
158
|
+
return Text(f"({self.info_str()})", style='wheat4 dim')
|
|
159
|
+
else:
|
|
160
|
+
return Text(f"({self.info_str()})", style='plum4 dim')
|
|
161
|
+
elif self.is_a_mystery():
|
|
162
|
+
return Text(QUESTION_MARKS, style='honeydew2 bold')
|
|
163
|
+
elif self.info_str() is None:
|
|
164
|
+
if self.name in MAILING_LISTS:
|
|
165
|
+
return Text('(mailing list)', style=f"pale_turquoise4 dim")
|
|
166
|
+
elif self.category():
|
|
167
|
+
return Text(QUESTION_MARKS, style=self.style())
|
|
168
|
+
else:
|
|
169
|
+
return None
|
|
170
|
+
else:
|
|
171
|
+
return Text(self.info_str())
|
|
172
|
+
|
|
173
|
+
def is_a_mystery(self) -> bool:
|
|
174
|
+
"""Return True if this is someone we theroetically could know more about."""
|
|
175
|
+
return self.is_unstyled() and not (self.is_email_address() or self.info_str() or self.is_uninteresting_cc)
|
|
176
|
+
|
|
177
|
+
def is_email_address(self) -> bool:
|
|
178
|
+
return '@' in (self.name or '')
|
|
179
|
+
|
|
180
|
+
def is_linkable(self) -> bool:
|
|
181
|
+
"""Return True if it's likely that EpsteinWeb has a page for this name."""
|
|
182
|
+
if self.name is None or ' ' not in self.name:
|
|
183
|
+
return False
|
|
184
|
+
elif self.is_email_address() or '/' in self.name or QUESTION_MARKS in self.name:
|
|
185
|
+
return False
|
|
186
|
+
elif self.name in INVALID_FOR_EPSTEIN_WEB:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
def is_unstyled(self) -> bool:
|
|
192
|
+
"""True if there's no highlight group for this name."""
|
|
193
|
+
return self.style() == DEFAULT_NAME_STYLE
|
|
194
|
+
|
|
195
|
+
def name_str(self) -> str:
|
|
196
|
+
return self.name or UNKNOWN
|
|
197
|
+
|
|
198
|
+
def name_link(self) -> Text:
|
|
199
|
+
"""Will only link if it's worth linking, otherwise just a Text object."""
|
|
200
|
+
if not self.is_linkable():
|
|
201
|
+
return self.name_txt()
|
|
202
|
+
else:
|
|
203
|
+
return Text.from_markup(link_markup(self.external_link(), self.name_str(), self.style()))
|
|
204
|
+
|
|
205
|
+
def name_txt(self) -> Text:
|
|
206
|
+
return styled_name(self.name)
|
|
207
|
+
|
|
208
|
+
def print_emails(self) -> list[Email]:
|
|
209
|
+
"""Print complete emails to or from a particular 'author'. Returns the Emails that were printed."""
|
|
210
|
+
print_centered(self.info_panel())
|
|
211
|
+
self.print_emails_table()
|
|
212
|
+
last_printed_email_was_duplicate = False
|
|
213
|
+
|
|
214
|
+
if self.category() == JUNK:
|
|
215
|
+
logger.warning(f"Not printing junk emailer '{self.name}'")
|
|
216
|
+
else:
|
|
217
|
+
for email in self._printable_emails():
|
|
218
|
+
if email.is_duplicate():
|
|
219
|
+
console.print(Padding(email.duplicate_file_txt().append('...'), (0, 0, 0, 4)))
|
|
220
|
+
last_printed_email_was_duplicate = True
|
|
221
|
+
else:
|
|
222
|
+
if last_printed_email_was_duplicate:
|
|
223
|
+
console.line()
|
|
224
|
+
|
|
225
|
+
console.print(email)
|
|
226
|
+
last_printed_email_was_duplicate = False
|
|
227
|
+
|
|
228
|
+
return self._printable_emails()
|
|
229
|
+
|
|
230
|
+
def print_emails_table(self) -> None:
|
|
231
|
+
table = Email.build_emails_table(self._unique_printable_emails(), self.name)
|
|
232
|
+
print_centered(Padding(table, (0, 5, 0, 5)))
|
|
233
|
+
|
|
234
|
+
if self.is_linkable():
|
|
235
|
+
print_centered(self.external_links_line())
|
|
236
|
+
|
|
237
|
+
console.line()
|
|
238
|
+
|
|
239
|
+
def sort_key(self) -> list[int | str]:
|
|
240
|
+
counts = [len(self.unique_emails()), int(self.has_any_epstein_emails())]
|
|
241
|
+
counts = [-1 * count for count in counts]
|
|
242
|
+
|
|
243
|
+
if args.sort_alphabetical:
|
|
244
|
+
return [self.name_str()] + counts
|
|
245
|
+
else:
|
|
246
|
+
return counts + [self.name_str()]
|
|
247
|
+
|
|
248
|
+
def style(self) -> str:
|
|
249
|
+
return get_style_for_name(self.name)
|
|
250
|
+
|
|
251
|
+
def unique_emails(self) -> Sequence[Email]:
|
|
252
|
+
return Document.without_dupes(self.emails)
|
|
253
|
+
|
|
254
|
+
def unique_emails_by(self) -> list[Email]:
|
|
255
|
+
return Document.without_dupes(self.emails_by())
|
|
256
|
+
|
|
257
|
+
def unique_emails_to(self) -> list[Email]:
|
|
258
|
+
return Document.without_dupes(self.emails_to())
|
|
259
|
+
|
|
260
|
+
def _printable_emails(self):
|
|
261
|
+
"""For Epstein we only want to print emails he sent to himself."""
|
|
262
|
+
if self.name == JEFFREY_EPSTEIN:
|
|
263
|
+
return [e for e in self.emails if e.is_note_to_self()]
|
|
264
|
+
else:
|
|
265
|
+
return self.emails
|
|
266
|
+
|
|
267
|
+
def _unique_printable_emails(self):
|
|
268
|
+
return Document.without_dupes(self._printable_emails())
|
|
269
|
+
|
|
270
|
+
def __str__(self):
|
|
271
|
+
return f"{self.name_str()}"
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def emailer_info_table(people: list['Person'], highlighted: list['Person'] | None = None, show_epstein_total: bool = False) -> Table:
|
|
275
|
+
"""Table of info about emailers."""
|
|
276
|
+
highlighted = highlighted or people
|
|
277
|
+
highlighted_names = [p.name for p in highlighted]
|
|
278
|
+
is_selection = len(people) != len(highlighted) or args.emailers_info
|
|
279
|
+
|
|
280
|
+
if is_selection:
|
|
281
|
+
title = Text(f"{EMAILER_INFO_TITLE} in This Order for the Highlighted Names (", style=TABLE_TITLE_STYLE)
|
|
282
|
+
title.append(THE_OTHER_PAGE_TXT).append(" has the rest)")
|
|
283
|
+
else:
|
|
284
|
+
title = f"{EMAILER_INFO_TITLE} in Chronological Order Based on Timestamp of First Email"
|
|
285
|
+
|
|
286
|
+
table = build_table(title)
|
|
287
|
+
table.add_column('First')
|
|
288
|
+
table.add_column('Name', max_width=24, no_wrap=True)
|
|
289
|
+
table.add_column('Category', justify='left', style='dim italic')
|
|
290
|
+
table.add_column('Num', justify='right', style='white')
|
|
291
|
+
table.add_column('Sent', justify='right', style='wheat4')
|
|
292
|
+
table.add_column('Recv', justify='right', style='wheat4')
|
|
293
|
+
table.add_column('Days', justify='right', style=TIMESTAMP_DIM)
|
|
294
|
+
table.add_column('Info', style='white italic')
|
|
295
|
+
current_year = 1990
|
|
296
|
+
current_year_month = current_year * 12
|
|
297
|
+
grey_idx = 0
|
|
298
|
+
|
|
299
|
+
for person in people:
|
|
300
|
+
earliest_email_date = person.earliest_email_date()
|
|
301
|
+
year_months = (earliest_email_date.year * 12) + earliest_email_date.month
|
|
302
|
+
|
|
303
|
+
# Color year rollovers more brightly
|
|
304
|
+
if current_year != earliest_email_date.year:
|
|
305
|
+
grey_idx = 0
|
|
306
|
+
elif current_year_month != year_months:
|
|
307
|
+
grey_idx = ((current_year_month - 1) % 12) + 1
|
|
308
|
+
|
|
309
|
+
current_year_month = year_months
|
|
310
|
+
current_year = earliest_email_date.year
|
|
311
|
+
|
|
312
|
+
table.add_row(
|
|
313
|
+
Text(str(earliest_email_date), style=f"grey{GREY_NUMBERS[0 if is_selection else grey_idx]}"),
|
|
314
|
+
person.name_txt(), # TODO: make link?
|
|
315
|
+
person.category_txt(),
|
|
316
|
+
f"{len(person.unique_emails() if show_epstein_total else person._unique_printable_emails())}",
|
|
317
|
+
Text(f"{len(person.unique_emails_by())}", style='dim' if len(person.unique_emails_by()) == 0 else ''),
|
|
318
|
+
Text(f"{len(person.unique_emails_to())}", style='dim' if len(person.unique_emails_to()) == 0 else ''),
|
|
319
|
+
f"{person.email_conversation_length_in_days()}",
|
|
320
|
+
person.info_txt() or '',
|
|
321
|
+
style='' if person.name in highlighted_names else 'dim',
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return table
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from epstein_files.util.constant.strings import QUESTION_MARKS, remove_question_marks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Name = str | None
|
|
4
4
|
|
|
5
5
|
# Texting Names
|
|
6
6
|
ANDRZEJ_DUDA = 'Andrzej Duda or entourage'
|
|
@@ -34,6 +34,7 @@ BARBRO_C_EHNBOM = 'Barbro C. Ehnbom'
|
|
|
34
34
|
BARRY_J_COHEN = 'Barry J. Cohen'
|
|
35
35
|
BENNET_MOSKOWITZ = 'Bennet Moskowitz'
|
|
36
36
|
BILL_SIEGEL = 'Bill Siegel'
|
|
37
|
+
BOB_CROWE = 'Bob Crowe'
|
|
37
38
|
BRAD_EDWARDS = 'Brad Edwards'
|
|
38
39
|
BRAD_KARP = 'Brad Karp'
|
|
39
40
|
BRAD_WECHSLER = 'Brad Wechsler'
|
|
@@ -78,6 +79,7 @@ JACK_GOLDBERGER = 'Jack Goldberger'
|
|
|
78
79
|
JACK_SCAROLA = 'Jack Scarola'
|
|
79
80
|
JACKIE_PERCZEK = 'Jackie Perczek'
|
|
80
81
|
JAMES_HILL = 'James Hill'
|
|
82
|
+
JANUSZ_BANASIAK = 'Janusz Banasiak'
|
|
81
83
|
JAY_LEFKOWITZ = 'Jay Lefkowitz'
|
|
82
84
|
JEAN_HUGUEN = 'Jean Huguen'
|
|
83
85
|
JEAN_LUC_BRUNEL = 'Jean Luc Brunel'
|
|
@@ -114,6 +116,7 @@ MARK_TRAMO = 'Mark Tramo'
|
|
|
114
116
|
MARTIN_NOWAK = 'Martin Nowak'
|
|
115
117
|
MARTIN_WEINBERG = "Martin Weinberg"
|
|
116
118
|
MASHA_DROKOVA = 'Masha Drokova'
|
|
119
|
+
MATTHEW_HILTZIK = 'Matthew Hiltzik'
|
|
117
120
|
MELANIE_SPINELLA = 'Melanie Spinella'
|
|
118
121
|
MERWIN_DELA_CRUZ = 'Merwin Dela Cruz'
|
|
119
122
|
MICHAEL_BUCHHOLTZ = 'Michael Buchholtz'
|
|
@@ -170,6 +173,8 @@ VINCENZO_IOZZO = 'Vincenzo Iozzo'
|
|
|
170
173
|
VINIT_SAHNI = 'Vinit Sahni'
|
|
171
174
|
ZUBAIR_KHAN = 'Zubair Khan'
|
|
172
175
|
|
|
176
|
+
UNKNOWN = '(unknown)'
|
|
177
|
+
|
|
173
178
|
# No communications but name is in the files
|
|
174
179
|
BILL_GATES = 'Bill Gates'
|
|
175
180
|
DONALD_TRUMP = 'Donald Trump'
|
|
@@ -202,30 +207,30 @@ INSIGHTS_POD = f"InsightsPod" # Zubair bots
|
|
|
202
207
|
MIT_MEDIA_LAB = 'MIT Media Lab'
|
|
203
208
|
NEXT_MANAGEMENT = 'Next Management LLC'
|
|
204
209
|
JP_MORGAN = 'JP Morgan'
|
|
205
|
-
OSBORNE_LLP = f"{IAN_OSBORNE} & Partners
|
|
206
|
-
ROTHSTEIN_ROSENFELDT_ADLER =
|
|
210
|
+
OSBORNE_LLP = f"{IAN_OSBORNE} & Partners" # Ian Osborne's PR firm
|
|
211
|
+
ROTHSTEIN_ROSENFELDT_ADLER = "Rothstein Rosenfeldt Adler (Rothstein was Roger Stone's partner)" # and a crook
|
|
207
212
|
TRUMP_ORG = 'Trump Organization'
|
|
208
213
|
UBS = 'UBS'
|
|
209
214
|
|
|
210
215
|
# First and last names that should be made part of a highlighting regex for emailers
|
|
211
216
|
NAMES_TO_NOT_HIGHLIGHT = """
|
|
212
|
-
al alan alfredo allen alex alexander amanda andres andrew
|
|
213
|
-
bard barrett barry bill black boris brad bruce
|
|
214
|
-
carolyn chris christina
|
|
217
|
+
al alain alan alfredo allen alex alexander amanda andres andrew anthony
|
|
218
|
+
bard barrett barry bennet bill black bob boris brad bruce
|
|
219
|
+
caroline carolyn chris christina cohen
|
|
215
220
|
dan daniel danny darren dave david donald
|
|
216
|
-
ed edward edwards enterprise enterprises entourage epstein eric erika etienne
|
|
217
|
-
faith fred friendly frost fuller
|
|
218
|
-
gerald george gold
|
|
219
|
-
harry hay heather henry hill hoffman
|
|
220
|
-
ian
|
|
221
|
+
ed edward edwards enforcement enterprise enterprises entourage epstein eric erika etienne
|
|
222
|
+
faith forget fred friendly frost fuller
|
|
223
|
+
gerald george gold gordon
|
|
224
|
+
haddad harry hay heather henry hill hoffman
|
|
225
|
+
ian ivan
|
|
221
226
|
jack james jay jean jeff jeffrey jennifer jeremy jessica joel john jon jonathan joseph jr
|
|
222
|
-
kahn karl kate katherine kelly ken kevin
|
|
227
|
+
kahn karl kate katherine kelly ken kevin krassner
|
|
223
228
|
larry laurie lawrence leon lesley linda link lisa
|
|
224
|
-
mann marc marie mark martin melanie michael mike miller mitchell miles morris moskowitz
|
|
225
|
-
nancy neal new nicole
|
|
229
|
+
mann marc marie mark martin matthew melanie michael mike miller mitchell miles morris moskowitz
|
|
230
|
+
nancy neal new nicole norman
|
|
226
231
|
owen
|
|
227
232
|
paul paula pen peter philip prince
|
|
228
|
-
randall reid richard robert rodriguez roger rosenberg ross roth roy rubin
|
|
233
|
+
randall rangel reid richard robert rodriguez roger rosenberg ross roth roy rubin
|
|
229
234
|
scott sean skip stanley stern stephen steve steven stone susan
|
|
230
235
|
the thomas tim tom tony tyler
|
|
231
236
|
victor
|
|
@@ -272,3 +277,29 @@ def constantize_name(name: str) -> str:
|
|
|
272
277
|
return f"'{name}'"
|
|
273
278
|
else:
|
|
274
279
|
return variable_name
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def extract_first_name(name: str) -> str:
|
|
283
|
+
if ' ' not in name:
|
|
284
|
+
return name
|
|
285
|
+
|
|
286
|
+
return name.removesuffix(f" {extract_last_name(name)}")
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def extract_last_name(name: str) -> str:
|
|
290
|
+
if ' ' not in name:
|
|
291
|
+
return name
|
|
292
|
+
|
|
293
|
+
first_last_names = remove_question_marks(name).strip().split()
|
|
294
|
+
|
|
295
|
+
if first_last_names[-1].startswith('Jr') and len(first_last_names[-1]) <= 3:
|
|
296
|
+
return ' '.join(first_last_names[-2:])
|
|
297
|
+
else:
|
|
298
|
+
return first_last_names[-1]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def reversed_name(name: str) -> str:
|
|
302
|
+
if ' ' not in name:
|
|
303
|
+
return name
|
|
304
|
+
|
|
305
|
+
return f"{extract_last_name(name)}, {extract_first_name(name)}"
|
|
@@ -13,6 +13,7 @@ TEXT_MSGS_HTML_PATH = HTML_DIR.joinpath('index.html')
|
|
|
13
13
|
WORD_COUNT_HTML_PATH = HTML_DIR.joinpath(f'communication_word_count_{EPSTEIN_FILES_NOV_2025}.html')
|
|
14
14
|
# EPSTEIN_WORD_COUNT_HTML_PATH = HTML_DIR.joinpath('epstein_texts_and_emails_word_count.html')
|
|
15
15
|
URLS_ENV = '.urls.env'
|
|
16
|
+
EMAILERS_TABLE_PNG_PATH = HTML_DIR.joinpath('emailers_info_table.png')
|
|
16
17
|
|
|
17
18
|
# Deployment URLS
|
|
18
19
|
# NOTE: don't rename these variables without changing deploy.sh!
|
|
@@ -9,8 +9,8 @@ ARTICLE = 'article'
|
|
|
9
9
|
BOOK = 'book'
|
|
10
10
|
BUSINESS = 'business'
|
|
11
11
|
CONFERENCE = 'conference'
|
|
12
|
-
ENTERTAINER = 'entertainer'
|
|
13
12
|
FINANCE = 'finance'
|
|
13
|
+
FRIEND = 'friend'
|
|
14
14
|
FLIGHT_LOG = 'flight log'
|
|
15
15
|
JOURNALIST = 'journalist'
|
|
16
16
|
JUNK = 'junk'
|
|
@@ -50,7 +50,7 @@ TEXT_MESSAGE = 'text message'
|
|
|
50
50
|
SiteType = Literal['email', 'text message']
|
|
51
51
|
|
|
52
52
|
# Styles
|
|
53
|
-
DEFAULT_NAME_STYLE = '
|
|
53
|
+
DEFAULT_NAME_STYLE = 'grey23'
|
|
54
54
|
TIMESTAMP_STYLE = 'turquoise4'
|
|
55
55
|
TIMESTAMP_DIM = f"turquoise4 dim"
|
|
56
56
|
|
|
@@ -76,7 +76,7 @@ MESSENGER_LOG_CLASS = 'MessengerLog'
|
|
|
76
76
|
OTHER_FILE_CLASS = 'OtherFile'
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
remove_question_marks = lambda name: QUESTION_MARKS_REGEX.sub('', name)
|
|
79
|
+
remove_question_marks = lambda name: QUESTION_MARKS_REGEX.sub('', name).strip()
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def indented(s: str, spaces: int = 4) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import urllib.parse
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Callable, Literal
|
|
4
4
|
|
|
5
5
|
from inflection import parameterize
|
|
6
6
|
from rich.text import Text
|
|
@@ -14,12 +14,13 @@ ARCHIVE_LINK_COLOR = 'slate_blue3'
|
|
|
14
14
|
TEXT_LINK = 'text_link'
|
|
15
15
|
|
|
16
16
|
# External site names
|
|
17
|
-
ExternalSite = Literal['epstein.media', 'epsteinify', 'EpsteinWeb', 'RollCall']
|
|
17
|
+
ExternalSite = Literal['epstein.media', 'epsteinify', 'EpsteinWeb', 'Jmail', 'RollCall', 'search X']
|
|
18
18
|
EPSTEIN_MEDIA = 'epstein.media'
|
|
19
19
|
EPSTEIN_WEB = 'EpsteinWeb'
|
|
20
20
|
EPSTEINIFY = 'epsteinify'
|
|
21
21
|
JMAIL = 'Jmail'
|
|
22
22
|
ROLLCALL = 'RollCall'
|
|
23
|
+
TWITTER = 'search X'
|
|
23
24
|
|
|
24
25
|
GH_PROJECT_URL = 'https://github.com/michelcrypt4d4mus/epstein_text_messages'
|
|
25
26
|
GH_MASTER_URL = f"{GH_PROJECT_URL}/blob/master"
|
|
@@ -32,6 +33,7 @@ extracted_file_url = lambda f: f"{EXTRACTS_BASE_URL}/{f}"
|
|
|
32
33
|
# External URLs
|
|
33
34
|
COFFEEZILLA_ARCHIVE_URL = 'https://journaliststudio.google.com/pinpoint/search?collection=061ce61c9e70bdfd'
|
|
34
35
|
COURIER_NEWSROOM_ARCHIVE_URL = 'https://journaliststudio.google.com/pinpoint/search?collection=092314e384a58618'
|
|
36
|
+
EPSTEIN_DOCS_URL = 'https://epstein-docs.github.io'
|
|
35
37
|
OVERSIGHT_REPUBLICANS_PRESSER_URL = 'https://oversight.house.gov/release/oversight-committee-releases-additional-epstein-estate-documents/'
|
|
36
38
|
RAW_OVERSIGHT_DOCS_GOOGLE_DRIVE_URL = 'https://drive.google.com/drive/folders/1hTNH5woIRio578onLGElkTWofUSWRoH_'
|
|
37
39
|
SUBSTACK_URL = 'https://cryptadamus.substack.com/p/i-made-epsteins-text-messages-great'
|
|
@@ -71,6 +73,15 @@ search_jmail_url = lambda txt: f"{JMAIL_URL}/search?q={urllib.parse.quote(txt)}"
|
|
|
71
73
|
search_twitter_url = lambda txt: f"https://x.com/search?q={urllib.parse.quote(txt)}&src=typed_query&f=live"
|
|
72
74
|
|
|
73
75
|
|
|
76
|
+
PERSON_LINK_BUILDERS: dict[ExternalSite, Callable[[str], str]] = {
|
|
77
|
+
EPSTEIN_MEDIA: epstein_media_person_url,
|
|
78
|
+
EPSTEIN_WEB: epstein_web_person_url,
|
|
79
|
+
EPSTEINIFY: epsteinify_name_url,
|
|
80
|
+
JMAIL: search_jmail_url,
|
|
81
|
+
TWITTER: search_twitter_url,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
74
85
|
def build_doc_url(base_url: str, filename_or_id: int | str, case: Literal['lower', 'title'] | None = None) -> str:
|
|
75
86
|
file_stem = coerce_file_stem(filename_or_id)
|
|
76
87
|
file_stem = file_stem.lower() if case == 'lower' or EPSTEIN_MEDIA in base_url else file_stem
|
|
@@ -111,3 +122,5 @@ def other_site_url() -> str:
|
|
|
111
122
|
|
|
112
123
|
|
|
113
124
|
CRYPTADAMUS_TWITTER = link_markup('https://x.com/cryptadamist', '@cryptadamist')
|
|
125
|
+
THE_OTHER_PAGE_MARKUP = link_markup(other_site_url(), 'the other page', style='light_slate_grey bold')
|
|
126
|
+
THE_OTHER_PAGE_TXT = Text.from_markup(THE_OTHER_PAGE_MARKUP)
|