epstein-files 1.2.0__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/person.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass, field
2
2
  from datetime import datetime, date
3
+ from typing import Sequence
3
4
 
4
5
  from rich.console import Group, RenderableType
5
6
  from rich.padding import Padding
@@ -18,13 +19,14 @@ from epstein_files.util.data import days_between, flatten, without_falsey
18
19
  from epstein_files.util.env import args
19
20
  from epstein_files.util.highlighted_group import (QUESTION_MARKS_TXT, HighlightedNames,
20
21
  get_highlight_group_for_name, get_style_for_name, styled_category, styled_name)
21
- from epstein_files.util.rich import GREY_NUMBERS, LAST_TIMESTAMP_STYLE, TABLE_TITLE_STYLE, build_table, console, join_texts, print_centered
22
+ from epstein_files.util.rich import GREY_NUMBERS, TABLE_TITLE_STYLE, build_table, console, join_texts, print_centered
22
23
 
23
24
  ALT_INFO_STYLE = 'medium_purple4'
24
25
  CC = 'cc:'
25
26
  MIN_AUTHOR_PANEL_WIDTH = 80
26
27
  EMAILER_INFO_TITLE = 'Email Conversations Will Appear'
27
- UNINTERESTING_CC_INFO = "CC: or BCC: recipient only"
28
+ UNINTERESTING_CC_INFO = "cc: or bcc: recipient only"
29
+ UNINTERESTING_CC_INFO_NO_CONTACT = f"{UNINTERESTING_CC_INFO}, no direct contact with Epstein"
28
30
 
29
31
  INVALID_FOR_EPSTEIN_WEB = JUNK_EMAILERS + MAILING_LISTS + [
30
32
  'ACT for America',
@@ -100,6 +102,10 @@ class Person:
100
102
  links = [self.external_link_txt(site) for site in PERSON_LINK_BUILDERS]
101
103
  return Text('', justify='center', style='dim').append(join_texts(links, join=' / ')) #, encloser='()'))#, encloser='‹›'))
102
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
+
103
109
  def highlight_group(self) -> HighlightedNames | None:
104
110
  return get_highlight_group_for_name(self.name)
105
111
 
@@ -132,7 +138,10 @@ class Person:
132
138
  if highlight_group and isinstance(highlight_group, HighlightedNames) and self.name:
133
139
  return highlight_group.info_for(self.name)
134
140
  elif self.is_uninteresting_cc:
135
- return UNINTERESTING_CC_INFO
141
+ if self.has_any_epstein_emails():
142
+ return UNINTERESTING_CC_INFO
143
+ else:
144
+ return UNINTERESTING_CC_INFO_NO_CONTACT
136
145
 
137
146
  def info_with_category(self) -> str:
138
147
  return ', '.join(without_falsey([self.category(), self.info_str()]))
@@ -144,13 +153,18 @@ class Person:
144
153
  return Text('(emails whose author or recipient could not be determined)', style=ALT_INFO_STYLE)
145
154
  elif self.category() == JUNK:
146
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')
147
161
  elif self.is_a_mystery():
148
- return Text(QUESTION_MARKS, style='magenta dim')
149
- elif self.is_uninteresting_cc and self.info_str() == UNINTERESTING_CC_INFO:
150
- return Text(f"({self.info_str()})", style='wheat4 dim')
162
+ return Text(QUESTION_MARKS, style='honeydew2 bold')
151
163
  elif self.info_str() is None:
152
164
  if self.name in MAILING_LISTS:
153
- return Text('(mailing list)', style=f"{self.style()} dim")
165
+ return Text('(mailing list)', style=f"pale_turquoise4 dim")
166
+ elif self.category():
167
+ return Text(QUESTION_MARKS, style=self.style())
154
168
  else:
155
169
  return None
156
170
  else:
@@ -214,8 +228,8 @@ class Person:
214
228
  return self._printable_emails()
215
229
 
216
230
  def print_emails_table(self) -> None:
217
- emails = [email for email in self._printable_emails() if not email.is_duplicate()] # Remove dupes
218
- print_centered(Padding(Email.build_emails_table(emails, self.name), (0, 5, 0, 5)))
231
+ table = Email.build_emails_table(self._unique_printable_emails(), self.name)
232
+ print_centered(Padding(table, (0, 5, 0, 5)))
219
233
 
220
234
  if self.is_linkable():
221
235
  print_centered(self.external_links_line())
@@ -223,7 +237,7 @@ class Person:
223
237
  console.line()
224
238
 
225
239
  def sort_key(self) -> list[int | str]:
226
- counts = [len(self.unique_emails())]
240
+ counts = [len(self.unique_emails()), int(self.has_any_epstein_emails())]
227
241
  counts = [-1 * count for count in counts]
228
242
 
229
243
  if args.sort_alphabetical:
@@ -234,14 +248,14 @@ class Person:
234
248
  def style(self) -> str:
235
249
  return get_style_for_name(self.name)
236
250
 
237
- def unique_emails(self) -> list[Email]:
238
- return [email for email in self.emails if not email.is_duplicate()]
251
+ def unique_emails(self) -> Sequence[Email]:
252
+ return Document.without_dupes(self.emails)
239
253
 
240
254
  def unique_emails_by(self) -> list[Email]:
241
- return [email for email in self.emails_by() if not email.is_duplicate()]
255
+ return Document.without_dupes(self.emails_by())
242
256
 
243
257
  def unique_emails_to(self) -> list[Email]:
244
- return [email for email in self.emails_to() if not email.is_duplicate()]
258
+ return Document.without_dupes(self.emails_to())
245
259
 
246
260
  def _printable_emails(self):
247
261
  """For Epstein we only want to print emails he sent to himself."""
@@ -250,24 +264,27 @@ class Person:
250
264
  else:
251
265
  return self.emails
252
266
 
267
+ def _unique_printable_emails(self):
268
+ return Document.without_dupes(self._printable_emails())
269
+
253
270
  def __str__(self):
254
271
  return f"{self.name_str()}"
255
272
 
256
273
  @staticmethod
257
- def emailer_info_table(people: list['Person'], highlighted: list['Person'] | None = None) -> Table:
274
+ def emailer_info_table(people: list['Person'], highlighted: list['Person'] | None = None, show_epstein_total: bool = False) -> Table:
258
275
  """Table of info about emailers."""
259
276
  highlighted = highlighted or people
260
277
  highlighted_names = [p.name for p in highlighted]
261
- is_selection = len(people) != len(highlighted) or args.emailers_info_png
278
+ is_selection = len(people) != len(highlighted) or args.emailers_info
262
279
 
263
280
  if is_selection:
264
- title = Text(f"{EMAILER_INFO_TITLE} in This Order for the Highlighted Names (see ", style=TABLE_TITLE_STYLE)
265
- title.append(THE_OTHER_PAGE_TXT).append(" for the rest)")
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)")
266
283
  else:
267
284
  title = f"{EMAILER_INFO_TITLE} in Chronological Order Based on Timestamp of First Email"
268
285
 
269
286
  table = build_table(title)
270
- table.add_column('Start')
287
+ table.add_column('First')
271
288
  table.add_column('Name', max_width=24, no_wrap=True)
272
289
  table.add_column('Category', justify='left', style='dim italic')
273
290
  table.add_column('Num', justify='right', style='white')
@@ -296,55 +313,12 @@ class Person:
296
313
  Text(str(earliest_email_date), style=f"grey{GREY_NUMBERS[0 if is_selection else grey_idx]}"),
297
314
  person.name_txt(), # TODO: make link?
298
315
  person.category_txt(),
299
- f"{len(person._printable_emails())}",
300
- f"{len(person.unique_emails_by())}",
301
- f"{len(person.unique_emails_to())}",
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 ''),
302
319
  f"{person.email_conversation_length_in_days()}",
303
320
  person.info_txt() or '',
304
321
  style='' if person.name in highlighted_names else 'dim',
305
322
  )
306
323
 
307
324
  return table
308
-
309
- @staticmethod
310
- def emailer_stats_table(people: list['Person']) -> Table:
311
- email_authors = [p for p in people if p.emails_by() and p.name]
312
- all_emails = Document.uniquify(flatten([p.unique_emails() for p in people]))
313
- attributed_emails = [email for email in all_emails if email.author]
314
- footer = f"(identified {len(email_authors)} authors of {len(attributed_emails):,}"
315
- footer = f"{footer} out of {len(attributed_emails):,} emails)"
316
-
317
- counts_table = build_table(
318
- f"All {len(email_authors)} People Who Sent or Received an Email in the Files",
319
- caption=footer,
320
- cols=[
321
- 'Name',
322
- {'name': 'Count', 'justify': 'right', 'style': 'bold bright_white'},
323
- {'name': 'Sent', 'justify': 'right', 'style': 'gray74'},
324
- {'name': 'Recv', 'justify': 'right', 'style': 'gray74'},
325
- {'name': 'First', 'style': TIMESTAMP_STYLE},
326
- {'name': 'Last', 'style': LAST_TIMESTAMP_STYLE},
327
- {'name': 'Days', 'justify': 'right', 'style': 'dim'},
328
- JMAIL,
329
- EPSTEIN_MEDIA,
330
- EPSTEIN_WEB,
331
- 'Twitter',
332
- ]
333
- )
334
-
335
- for person in sorted(people, key=lambda person: person.sort_key()):
336
- counts_table.add_row(
337
- person.name_link(),
338
- f"{len(person.unique_emails()):,}",
339
- f"{len(person.unique_emails_by()):,}",
340
- f"{len(person.unique_emails_to()):,}",
341
- str(person.earliest_email_date()),
342
- str(person.last_email_date()),
343
- f"{person.email_conversation_length_in_days()}",
344
- person.external_link_txt(JMAIL),
345
- person.external_link_txt(EPSTEIN_MEDIA) if person.is_linkable() else '',
346
- person.external_link_txt(EPSTEIN_WEB) if person.is_linkable() else '',
347
- person.external_link_txt(TWITTER),
348
- )
349
-
350
- return counts_table
@@ -214,11 +214,11 @@ UBS = 'UBS'
214
214
 
215
215
  # First and last names that should be made part of a highlighting regex for emailers
216
216
  NAMES_TO_NOT_HIGHLIGHT = """
217
- al alain alan alfredo allen alex alexander amanda andres andrew
218
- bard barrett barry bill black bob boris brad bruce
219
- 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
220
220
  dan daniel danny darren dave david donald
221
- ed edward edwards enterprise enterprises entourage epstein eric erika etienne
221
+ ed edward edwards enforcement enterprise enterprises entourage epstein eric erika etienne
222
222
  faith forget fred friendly frost fuller
223
223
  gerald george gold gordon
224
224
  haddad harry hay heather henry hill hoffman
@@ -226,8 +226,8 @@ NAMES_TO_NOT_HIGHLIGHT = """
226
226
  jack james jay jean jeff jeffrey jennifer jeremy jessica joel john jon jonathan joseph jr
227
227
  kahn karl kate katherine kelly ken kevin krassner
228
228
  larry laurie lawrence leon lesley linda link lisa
229
- mann marc marie mark martin melanie michael mike miller mitchell miles morris moskowitz
230
- 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
231
231
  owen
232
232
  paul paula pen peter philip prince
233
233
  randall rangel reid richard robert rodriguez roger rosenberg ross roth roy rubin
@@ -296,3 +296,10 @@ def extract_last_name(name: str) -> str:
296
296
  return ' '.join(first_last_names[-2:])
297
297
  else:
298
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)}"
@@ -9,7 +9,6 @@ ARTICLE = 'article'
9
9
  BOOK = 'book'
10
10
  BUSINESS = 'business'
11
11
  CONFERENCE = 'conference'
12
- ENTERTAINER = 'entertainer'
13
12
  FINANCE = 'finance'
14
13
  FRIEND = 'friend'
15
14
  FLIGHT_LOG = 'flight log'
@@ -33,6 +33,7 @@ extracted_file_url = lambda f: f"{EXTRACTS_BASE_URL}/{f}"
33
33
  # External URLs
34
34
  COFFEEZILLA_ARCHIVE_URL = 'https://journaliststudio.google.com/pinpoint/search?collection=061ce61c9e70bdfd'
35
35
  COURIER_NEWSROOM_ARCHIVE_URL = 'https://journaliststudio.google.com/pinpoint/search?collection=092314e384a58618'
36
+ EPSTEIN_DOCS_URL = 'https://epstein-docs.github.io'
36
37
  OVERSIGHT_REPUBLICANS_PRESSER_URL = 'https://oversight.house.gov/release/oversight-committee-releases-additional-epstein-estate-documents/'
37
38
  RAW_OVERSIGHT_DOCS_GOOGLE_DRIVE_URL = 'https://drive.google.com/drive/folders/1hTNH5woIRio578onLGElkTWofUSWRoH_'
38
39
  SUBSTACK_URL = 'https://cryptadamus.substack.com/p/i-made-epsteins-text-messages-great'
@@ -661,6 +661,7 @@ EMAILS_CONFIG = [
661
661
  EmailCfg(id='032023', is_fwded_article=True, duplicate_ids=['032012']), # American-Israeli Cooperative Enterprise Newsletter
662
662
  EmailCfg(id='021758', is_fwded_article=True, duplicate_ids=['030616']), # Radar Online article about Epstein's early prison release
663
663
  EmailCfg(id='033297', is_fwded_article=True, duplicate_ids=['033586']), # Sultan Sulayem fwding article about Trump and Russia
664
+ EmailCfg(id='030983', is_fwded_article=True), # Power Line blog Alex Acosta and Jeffrey Epstein Plea Deal Analysis
664
665
  EmailCfg(id='031774', is_fwded_article=True), # Krassner fwd of Palmer Report article
665
666
  EmailCfg(id='033345', is_fwded_article=True), # Krassner fwd of Palmer Report article
666
667
  EmailCfg(id='029903', is_fwded_article=True), # Krassner fwd of Ann Coulter article about Epstein
@@ -874,7 +875,7 @@ TWEET = 'tweet'
874
875
  # Legal cases
875
876
  BRUNEL_V_EPSTEIN = f"{JEAN_LUC_BRUNEL} v. {JEFFREY_EPSTEIN} and Tyler McDonald d/b/a YI.org"
876
877
  EDWARDS_V_DERSHOWITZ = f"{BRAD_EDWARDS} & {PAUL_G_CASSELL} v. {ALAN_DERSHOWITZ}"
877
- EPSTEIN_V_ROTHSTEIN_EDWARDS = f"Epstein v. Scott Rothstein, {BRAD_EDWARDS}, and L.M."
878
+ EPSTEIN_V_ROTHSTEIN_EDWARDS = f"Epstein v. Scott Rothstein, {BRAD_EDWARDS}, & L.M."
878
879
  GIUFFRE_V_DERSHOWITZ = f"{VIRGINIA_GIUFFRE} v. {ALAN_DERSHOWITZ}"
879
880
  GIUFFRE_V_EPSTEIN = f"{VIRGINIA_GIUFFRE} v. {JEFFREY_EPSTEIN}"
880
881
  GIUFFRE_V_MAXWELL = f"{VIRGINIA_GIUFFRE} v. {GHISLAINE_MAXWELL}"
@@ -1723,6 +1724,7 @@ UNINTERESTING_EMAILERS = FLIGHT_IN_2012_PEOPLE + IRAN_DEAL_RECIPIENTS + [
1723
1724
  'Oliver Goodenough', # Robert Trivers CC
1724
1725
  'Peter Aldhous', # Lawrence Krauss CC
1725
1726
  'Players2', # Hoffenberg CC
1727
+ 'Police Code Enforcement', # Kirk Blouin / John Page CC
1726
1728
  'Sam Harris', # Lawrence Krauss CC
1727
1729
  SAMUEL_LEFF, # Random CC
1728
1730
  'Sean T Lehane', # Random CC
@@ -29,7 +29,7 @@ escape_single_quotes = lambda text: text.replace("'", r"\'")
29
29
  iso_timestamp = lambda dt: dt.isoformat().replace('T', ' ')
30
30
  days_between = lambda dt1, dt2: (dt2 - dt1).days + 1
31
31
  days_between_str = lambda dt1, dt2: f"{days_between(dt1, dt2)} day" + ('s' if days_between(dt1, dt2) > 1 else '')
32
- remove_zero_time_from_timestamp_str = lambda dt: dt.isoformat().removesuffix('T00:00:00')
32
+ remove_zero_time = lambda dt: dt.isoformat().removesuffix('T00:00:00')
33
33
  uniquify = lambda _list: list(set(_list))
34
34
  without_falsey = lambda _list: [e for e in _list if e]
35
35
 
@@ -8,7 +8,7 @@ from dateutil.parser import parse
8
8
 
9
9
  from epstein_files.util.constant.names import *
10
10
  from epstein_files.util.constant.strings import *
11
- from epstein_files.util.data import remove_zero_time_from_timestamp_str, without_falsey
11
+ from epstein_files.util.data import remove_zero_time, without_falsey
12
12
 
13
13
  DuplicateType = Literal['earlier', 'quoted', 'redacted', 'same']
14
14
  Metadata = dict[str, bool | datetime | int | str | list[str | None] |dict[str, bool | str]]
@@ -81,6 +81,7 @@ class DocCfg:
81
81
  dupe_type: DuplicateType | None = None
82
82
  duplicate_ids: list[str] = field(default_factory=list)
83
83
  duplicate_of_id: str | None = None
84
+ is_attribution_uncertain: bool = False
84
85
  is_interesting: bool | None = None
85
86
  is_synthetic: bool = False
86
87
  timestamp: datetime | None = None
@@ -162,7 +163,7 @@ class DocCfg:
162
163
  elif _field.name == 'timestamp' and self.date is not None:
163
164
  continue # Don't print both timestamp and date
164
165
  elif isinstance(value, datetime):
165
- value_str = remove_zero_time_from_timestamp_str(value)
166
+ value_str = remove_zero_time(value)
166
167
  add_prop(_field, f"parse('{value_str}')" if CONSTANTIZE_NAMES else f"'{value}'")
167
168
  elif isinstance(value, str):
168
169
  if "'" in value:
@@ -206,7 +207,6 @@ class CommunicationCfg(DocCfg):
206
207
  is_attribution_uncertain (bool): True if we have a good idea of who the author is but are not 100% certain
207
208
  """
208
209
  attribution_reason: str | None = None
209
- is_attribution_uncertain: bool = False
210
210
 
211
211
  def __repr__(self) -> str:
212
212
  return super().__repr__()
epstein_files/util/env.py CHANGED
@@ -38,7 +38,7 @@ output.add_argument('--all-emails', '-ae', action='store_true', help='all the em
38
38
  output.add_argument('--all-other-files', '-ao', action='store_true', help='all the non-email, non-text msg files instead of just the interesting ones')
39
39
  parser.add_argument('--build', '-b', nargs="?", default=None, const=DEFAULT_FILE, help='write output to HTML file')
40
40
  output.add_argument('--email-timeline', action='store_true', help='print a table of all emails in chronological order')
41
- output.add_argument('--emailers-info-png', action='store_true', help='write a .png of the emeailers info table')
41
+ output.add_argument('--emailers-info', action='store_true', help='write a .png of the eeailers info table')
42
42
  output.add_argument('--json-files', action='store_true', help='pretty print all the raw JSON data files in the collection and exit')
43
43
  output.add_argument('--json-metadata', action='store_true', help='dump JSON metadata for all files and exit')
44
44
  output.add_argument('--output-emails', '-oe', action='store_true', help='generate emails section')
@@ -67,9 +67,9 @@ debug.add_argument('--suppress-logs', '-sl', action='store_true', help='set debu
67
67
  args = parser.parse_args()
68
68
  is_html_script = parser.prog in HTML_SCRIPTS
69
69
 
70
- args.build = args.build or args.emailers_info_png
70
+ args.build = args.build
71
71
  args.debug = args.deep_debug or args.debug or is_env_var_set('DEBUG')
72
- args.names = [None if n == 'None' else n for n in (args.names or [])]
72
+ args.names = [None if n == 'None' else n.strip() for n in (args.names or [])]
73
73
  args.output_emails = args.output_emails or args.all_emails
74
74
  args.output_other = args.output_other or args.all_other_files or args.uninteresting
75
75
  args.overwrite_pickle = args.overwrite_pickle or (is_env_var_set('OVERWRITE_PICKLE') and not is_env_var_set('PICKLED'))
@@ -83,7 +83,7 @@ if is_html_script:
83
83
  if any([is_output_arg(arg) and val for arg, val in vars(args).items()]):
84
84
  if args.email_timeline:
85
85
  exit_with_error(f"--email-timeline option is mutually exlusive with other output options")
86
- elif not args.email_timeline:
86
+ elif not args.email_timeline and not args.emailers_info:
87
87
  logger.warning(f"No output section chosen; outputting default selection of texts, selected emails, and other files...")
88
88
  args.output_texts = args.output_emails = args.output_other = True
89
89