richqueue 1.7__tar.gz → 1.10__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: richqueue
3
- Version: 1.7
3
+ Version: 1.10
4
4
  Summary: RichQueue
5
5
  Project-URL: Homepage, https://github.com/mwinokan/RichQueue/issues
6
6
  Project-URL: Bug Tracker, https://github.com/mwinokan/RichQueue/issues
@@ -48,6 +48,10 @@ To see the last `X` weeks history:
48
48
 
49
49
  ```rq --hist X```
50
50
 
51
+ To see history for a given time period, e.g.:
52
+
53
+ ```rq --hist '3 days'```
54
+
51
55
  To list available nodes on the cluster:
52
56
 
53
57
  ```rq --idle```
@@ -31,6 +31,10 @@ To see the last `X` weeks history:
31
31
 
32
32
  ```rq --hist X```
33
33
 
34
+ To see history for a given time period, e.g.:
35
+
36
+ ```rq --hist '3 days'```
37
+
34
38
  To list available nodes on the cluster:
35
39
 
36
40
  ```rq --idle```
@@ -3,7 +3,7 @@ requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
  [project]
5
5
  name = "richqueue"
6
- version = "1.7"
6
+ version = "1.10"
7
7
  authors = [
8
8
  { name = "Max Winokan", email = "max@winokan.com" },
9
9
  ]
@@ -32,4 +32,3 @@ include = [
32
32
  [project.scripts]
33
33
  rq = "richqueue.main:main"
34
34
  res = "richqueue.log:main"
35
- confucius = "richqueue.wisdom:main"
@@ -1,5 +1,5 @@
1
1
  from typer import Typer, Option
2
- from typing import Optional
2
+ from typing import Optional, List
3
3
  from typing_extensions import Annotated
4
4
  from .layout import dual_layout
5
5
  from rich.live import Live
@@ -14,6 +14,8 @@ from .slurm import (
14
14
  NotOnClusterError,
15
15
  )
16
16
 
17
+ import mrich
18
+
17
19
  # set up singletons
18
20
  app = Typer()
19
21
 
@@ -34,19 +36,15 @@ def show(
34
36
  no_loop: Annotated[
35
37
  bool, Option("-nl", "--no-loop", help="Don't show a live-updating screen")
36
38
  ] = False,
37
- hist: Annotated[
38
- int, Option("-h", "--hist", help="Show historical jobs from the last X weeks")
39
- ] = None,
40
- hist_unit: Annotated[
41
- str,
42
- Option("-hu", "--hist-unit", help="Show historical jobs from the last X weeks"),
43
- ] = "weeks",
39
+ hist: Optional[List[str]] = Option(
40
+ None,
41
+ "--hist",
42
+ "-h",
43
+ help="Show historical jobs from the preceding specified time period, e.g. '4 weeks'",
44
+ ),
44
45
  job: Annotated[
45
46
  int, Option("-j", "--job", help="Show details for a specific job")
46
47
  ] = None,
47
- wisdom: Annotated[
48
- bool, Option("-w", "--wisdom", help="Show a confucius quote on exit")
49
- ] = False,
50
48
  ):
51
49
 
52
50
  loop = True
@@ -59,7 +57,7 @@ def show(
59
57
  "idle": idle,
60
58
  "no_loop": no_loop,
61
59
  "hist": hist,
62
- "hist_unit": hist_unit,
60
+ "hist_unit": "weeks",
63
61
  "screen": screen,
64
62
  "disappear": disappear,
65
63
  "job": job,
@@ -67,6 +65,12 @@ def show(
67
65
 
68
66
  # console.print(kwargs)
69
67
 
68
+ if hist:
69
+ hist, hist_unit = parse_history_string(hist)
70
+
71
+ kwargs["hist"] = hist
72
+ kwargs["hist_unit"] = hist_unit
73
+
70
74
  if no_loop:
71
75
  loop = False
72
76
 
@@ -106,10 +110,6 @@ def show(
106
110
  time.sleep(1)
107
111
  except KeyboardInterrupt:
108
112
  live.stop()
109
- if wisdom:
110
- from .wisdom import print_random_quote
111
-
112
- print_random_quote()
113
113
 
114
114
  # static layout
115
115
  else:
@@ -138,6 +138,22 @@ def nickname():
138
138
  raise NotImplementedError
139
139
 
140
140
 
141
+ def parse_history_string(hist: str, unit: str = "week") -> (int, str):
142
+ """Parse one or two part string into integer value and string unit"""
143
+
144
+ hist = " ".join(hist)
145
+ hist = hist.split()
146
+
147
+ assert len(hist) <= 2, "wrong number of 'hist' values"
148
+
149
+ if len(hist) == 2:
150
+ unit = hist[1]
151
+
152
+ hist = int(hist[0])
153
+
154
+ return hist, unit
155
+
156
+
141
157
  # start Typer app
142
158
  def main():
143
159
  app()
@@ -33,25 +33,15 @@ def get_layout_pair(user: str | None, console_height=None, **kwargs):
33
33
 
34
34
  df = combined_df(user=user, **kwargs)
35
35
 
36
- # if df is None:
37
- # running = Panel(Text("[bold]No active jobs"), expand=False)
38
- # else:
39
-
40
36
  n_rows = len(df)
41
37
  n_running = len(df[df["job_state"] == "RUNNING"])
42
38
  n_pending = len(df[df["job_state"] == "PENDING"])
43
39
 
44
40
  hide_pending = False
45
41
 
46
- # print(console_height)
47
-
48
42
  if console_height is None:
49
43
  console_height = console.size.height
50
44
 
51
- # print(console_height)
52
- # print(n_running)
53
- # print(n_pending)
54
-
55
45
  history_df = df[~df["job_state"].isin(["RUNNING", "PENDING"])]
56
46
 
57
47
  if n_running + n_pending > 0:
@@ -71,7 +61,6 @@ def get_layout_pair(user: str | None, console_height=None, **kwargs):
71
61
 
72
62
  # hide pending?
73
63
  elif n_rows - n_pending < max_rows:
74
- # running_df = running_df[running_df["job_state"]=="RUNNING"]
75
64
  running_limit = None
76
65
  history_limit = None
77
66
  hide_pending = n_pending
@@ -410,15 +399,14 @@ def combined_df(**kwargs) -> "DataFrame":
410
399
  def add_run_time(df):
411
400
 
412
401
  def inner(row):
413
- # print(f'{row.end_time=} {type(row.end_time)=}')
414
402
 
415
403
  if row.job_state == "PENDING":
416
404
  return ""
417
405
 
418
- if isnull(row.end_time):
419
- return human_timedelta(datetime.datetime.now() - row.start_time)
420
- else:
421
- return human_timedelta(row.end_time - row.start_time)
406
+ if "end_time" not in row or isnull(row.end_time):
407
+ row.end_time = datetime.datetime.now()
408
+
409
+ return human_timedelta(row.end_time - row.start_time)
422
410
 
423
411
  return df.apply(inner, axis=1)
424
412
 
@@ -38,7 +38,7 @@ def running_job_table(
38
38
 
39
39
  table = Table(title=title, box=None, header_style="")
40
40
 
41
- columns = RUNNING_JOB_COLUMNS[long]
41
+ columns = RUNNING_JOB_COLUMNS(long, user)
42
42
 
43
43
  for col in columns:
44
44
  col_data = COLUMNS[col]
@@ -99,13 +99,15 @@ def history_job_table(
99
99
  hist_string = f"last {hist} {hist_unit}"
100
100
 
101
101
  if not user:
102
- title = f"[bold]{count} previous jobs on {METADATA['cluster_name']} ({hist_string})"
102
+ title = (
103
+ f"[bold]{count} previous jobs on {METADATA['cluster_name']} ({hist_string})"
104
+ )
103
105
  else:
104
106
  title = f"[bold]{user}'s {count} previous jobs on {METADATA['cluster_name']} ({hist_string})"
105
107
 
106
108
  table = Table(title=title, box=None, header_style="")
107
109
 
108
- columns = HISTORY_JOB_COLUMNS[long]
110
+ columns = HISTORY_JOB_COLUMNS(long, user)
109
111
 
110
112
  for col in columns:
111
113
  col_data = COLUMNS[col]
@@ -283,7 +285,9 @@ def color_by_state(state):
283
285
  elif state == "IDLE":
284
286
  return "[bold bright_green]Idle"
285
287
  elif state == "PENDING":
286
- return "[bright_red]Preempted"
288
+ return "[bright_yellow]Preempted"
289
+ elif state == "NODE_FAIL":
290
+ return "[bold bright_red]Node Failure"
287
291
  elif state == "PREEMPTED":
288
292
  return "[bright_yellow]Pending"
289
293
  elif state == "MIXED":
@@ -387,55 +391,118 @@ JOB_COLUMNS = {
387
391
  ],
388
392
  }
389
393
 
390
- RUNNING_JOB_COLUMNS = {
391
- True: [
392
- "job_id",
393
- "name",
394
- "node_count",
395
- "cpus",
396
- "submit_time",
397
- "start_time",
398
- "run_time",
399
- "time_limit",
400
- "partition",
401
- "nodes",
402
- "job_state",
403
- ],
404
- False: [
405
- "job_id",
406
- "name",
407
- "node_count",
408
- "cpus",
409
- "start_time",
410
- "run_time",
411
- "time_limit",
412
- "job_state",
413
- ],
414
- }
415
394
 
416
- HISTORY_JOB_COLUMNS = {
417
- True: [
418
- "job_id",
419
- "name",
420
- # "node_count",
421
- # "cpus",
422
- "submit_time",
423
- "start_time",
424
- "run_time",
425
- "partition",
426
- "nodes",
427
- "job_state",
428
- ],
429
- False: [
430
- "job_id",
431
- "name",
432
- # "node_count",
433
- # "cpus",
434
- "start_time",
435
- "run_time",
436
- "job_state",
437
- ],
438
- }
395
+ def RUNNING_JOB_COLUMNS(long, user):
396
+ if user:
397
+ if long:
398
+ return [
399
+ "job_id",
400
+ "name",
401
+ "node_count",
402
+ "cpus",
403
+ "submit_time",
404
+ "start_time",
405
+ "run_time",
406
+ "time_limit",
407
+ "partition",
408
+ "nodes",
409
+ "job_state",
410
+ ]
411
+
412
+ else:
413
+ return [
414
+ "job_id",
415
+ "name",
416
+ "node_count",
417
+ "cpus",
418
+ "start_time",
419
+ "run_time",
420
+ "time_limit",
421
+ "job_state",
422
+ ]
423
+ else:
424
+ if long:
425
+ return [
426
+ "job_id",
427
+ "user_name",
428
+ "name",
429
+ "node_count",
430
+ "cpus",
431
+ "submit_time",
432
+ "start_time",
433
+ "run_time",
434
+ "time_limit",
435
+ "partition",
436
+ "nodes",
437
+ "job_state",
438
+ ]
439
+
440
+ else:
441
+ return [
442
+ "job_id",
443
+ "user_name",
444
+ "name",
445
+ "node_count",
446
+ "cpus",
447
+ "start_time",
448
+ "run_time",
449
+ "time_limit",
450
+ "job_state",
451
+ ]
452
+
453
+
454
+ def HISTORY_JOB_COLUMNS(long, user):
455
+ if user:
456
+ if long:
457
+ return [
458
+ "job_id",
459
+ "name",
460
+ # "node_count",
461
+ # "cpus",
462
+ "submit_time",
463
+ "start_time",
464
+ "run_time",
465
+ "partition",
466
+ "nodes",
467
+ "job_state",
468
+ ]
469
+ else:
470
+ return [
471
+ "job_id",
472
+ "name",
473
+ # "node_count",
474
+ # "cpus",
475
+ "start_time",
476
+ "run_time",
477
+ "job_state",
478
+ ]
479
+ else:
480
+ if long:
481
+ return [
482
+ "job_id",
483
+ "user_name",
484
+ "name",
485
+ # "node_count",
486
+ # "cpus",
487
+ "submit_time",
488
+ "start_time",
489
+ "run_time",
490
+ "partition",
491
+ "nodes",
492
+ "job_state",
493
+ ]
494
+ else:
495
+ return [
496
+ "job_id",
497
+ "user_name",
498
+ "name",
499
+ # "node_count",
500
+ # "cpus",
501
+ "start_time",
502
+ "run_time",
503
+ "job_state",
504
+ ]
505
+
439
506
 
440
507
  NODE_COLUMNS = {
441
508
  True: [
@@ -585,7 +652,10 @@ COLUMNS = {
585
652
  "no_wrap": True,
586
653
  },
587
654
  "threads_per_core": {"header": "#Threads/core", "style": "magenta"},
588
- "user_name": {"header": "User Group", "style": "green_yellow"},
655
+ "user_name": {
656
+ "header": "[bold underline green_yellow]User",
657
+ "style": "green_yellow",
658
+ },
589
659
  }
590
660
 
591
661
  FORMATTERS = {
@@ -1 +0,0 @@
1
- gAAAAABnEpbvOgLsVhspN5M4SH7oIIjr2t8BaSv96xKOIeaZfAOABpiQdZiLzMWre3LD14ZH-i0VeydLW6JSaepYtOW6qhnOtZMUh5LvXBubEhPPQkR6w3F7rMOLQQOsBHxHBsYktVf9-733DnWH7KgSMrUT5StcpFmM9mPG--hcJMWfFi5YsOkmlcMBn5elcku9AJRkgyYKzcxdZnX1uWdPJk3C2j-lzysAhecW4sLb7ygvFwvqsz2GFlYebf2JUHjTulYGuU7UuSh-N2NG1X5dZX3mRi9Pn8IDRvidVowdUdQq7IcAxSp8AxdF4OqS1IhIcUJ1xVlRe8qRvu5jtxP3NyBV39zBgfAp1AZInm2CoxLAzGL1_Lb5EeO0ev9PGu6eMSvHfenBLWS_WnB24z0dwfWF9bQPeyOoKbm-0GSBQPSjIJPOaCBfhkFxH0SaRdYVuwgqzCGZXQL_j8OjpDHi_XSWMXCFWuswE4WS2kiu4g46M4mQP-HOR4EuwibdxiPxhvuHwibnEoK0pgxG9wyQeowejaOm-Au7avwd2x_M_S1aY3c4TtI_Immx4BqBuBMFWDOgdwLzHuNgagU5NvYNsXElow0bwlIERjboNvq3Pn3m4kHRF3-RoxmiXuhNkN0Ho11GKGaylbFSTXHrIJUIDKSjzeQnVOPLYOb9JvqL9RI7HrikFIUASJTogH4whfLKNikTaHrSkDOpR5PiZhVTNARkZI9xZ6vEcmDemFTLsQ8RZ22a5w-dG03i7dpnjAnfL9at7fcno56Fz83tev_pb8EIY32SoaEGKjbz_jLIO2nKM238LttWfIefy93Q6Uk_W7Jc3gtmcWj1sQ-KEJ7qmk4QAC--lGNnF8AD1HvD6jyroKzGzRJCbef76kG_6m_zlsZXVSIctA2var1vIpHWMa459jIxpzNiydldvh9NxcP3GiLsFbUDVs3MtzxWz7Onqy2KlnDCFWS76fZIvwuHzdryKEEYEUebfEIxwFLK4dAH9gx6lK0HQY1_Q0NV3MEi5jTcc_I3vpLeiu3xTjmPAt14s_UESjjOoXqxgtG7EQwxX8j2NJdqNNf1NeeHN1Hz4ZKy1XB27hjFqI_bF_f-bW4UgZEddGdbwhjDH8AefKuOTXoi764xlhklvhBLBTfpnqzGK6Qy1tjG-QbbdllEzKlYyMHqQwXvUrYjmy3OnmkkxVN03eYNyeH8tXhx8TSOSouQK7lX31SSK9Oo05H1XQLt6XbmZlXLOo7Yu3zeHBj5MADW7AqLyAk6WDmtGP-UJZviy1anetLqWEV862WuZFEcVSQAylPDgSLF9AZexb0YYQ3soPHeRsiwsjFeFeCLJeq_fN8mZ8CRRIn9bTulhWiYGJurq58XOzvZPiIjdOjY3F9sGRw6vetfFD9STKo_CUlvf_rYrUzUl5Z5S9PEC-yxUrDI4D25d-pCyum_nO9YtQlsv1f4MPwpwzFQvgdqBk43FVKmy8j2N0WHXg_aUVukKYV882ECvq1JT9kIp70yr1fZMKcDEDmgSrExBUoXI7hoN5HkDTKzbe_Mq76-OKH_1xLk4s4Hvn5Zd_v9s0ArshK5W9shQ10SltzHhegT_H9hSWrXQLi3BJx1OE77RRzXoa_eBIFSnnSNQeerbbuK08QsdAtYuk7ikmwg5gKy-8dnX73c9hYu5Au1f_7ZBH5OrlJJe2mU18dX1-uVsyfqYQsUNZSS9YVmeVfcvKFl_dyxVyzfSCQLTizeslpbs8SH9p2iDTL5d2D4ybF3_4AVUiBA_Am7Ael2mM0UZCth4zUyirePiGYXrcxXjQtSHyb_kcnwDYIg2WGhfe99A6EZ51755HTkQHtlLUlCv8zEo57Mz7ljAPH6E0f-fcyhpOJQshe7PV3xsBZL_sHrI5VnNkIQmP7onXQNVY86FphNgKcXCuTkw65I9TEAOEN5_y_YkQGT4Swm0xc83cS0h9jlvVLaBHLOecKXFCbVLCziHghD0Z9bNiuh-Mf7R0iJGDzzRN9AW3oMpcWoPTGuC0QNJWsQN9fXX783LkyxbsxtjFGli4USNhWnes4CNFFMbA5JaLhFPlERxRFMUUB6Du7kSQAvAB9ooxjYNvNSAgRQBc9EaKCVXVc8gDCGvfZcIdI90ysDbeERBJ1gnfKUtO5kf-ar7D1N5X5loOdW0oZ340fd84eHYBEKRJ7kFlr66bHMeEk8nUxjQ_ZUTcHjxDCnhYRrZkCw86zNiWriMKm4Nw7ErZ6kCQqQ3Zr1zZW1IkiMZSmv7qxBviZvAU_LsCeRfm1lqx3rlIIw9J4vfIUwTb5yZ-jtq_BBX6eYxXGCfnuZMm9pcAlWjAjFGOvGQp6JolkBckDNk6RimLaW33WCssEEntVTqiJYFFpXKmD57pfN0hoWEzslnESWS4xTOIxG1U-aJfC5VNzYwgZkMpaZeTuVGQNKUMk2zcHuXWBCtRi0uN3dO7JfgCn2Oeaa6ZPUAWz241VBzg1ka_2pENH5Eki4BKNyw9h928gIiDVVFnaKJydU-GGWuVXtdkhA-xMIuS7H8Mnqtgv2LVpNJyDu1FYKIT_zme1MLScLbuoiIi-ZcTC3Vs-vcroHUMdFPJjscvKZ5XtFQelzLiURcQ9DDzehmf-2Py_pgg5-pWGnNxhaYbV7Nzf68Xfoe5guQzjyGBSjR3Fzk7rIx9p1OfnRLD7zC9HNGy35CqA0K0Gzjym573rUkJ2QaircGt4BNRMqTbKr7dmt6xPIv7dOpK3eBNJvk-HVnz8hKqZTtYt9Kr9qivMuk3eOXjPaeSd6Ih4gUYDnIoU9V3nca-QQhYeaMt8ujfRHl1oE2eod23cZWpecqPitbaJRJQ5Bwmx-PeyjxIlYEby4WO8zr75A7NlVUu4kADDK8XtpQJDkyGMRZzuiqx5rhldyspmkP2zXtMV9tn-KQGZbUM1BW5ldXZZjdVrL3TZabM0vLbpnN_Pf8_3TYWMYOkc5TlNJ9NHbgo7epucW2wGV6O_dytO4TpLEDf4SXR2t7sE8ClJXrYbRvDJlOi3NK_799S7lHzNqw1RU_SnGPujgDRht2wWVUSSXOWijkwFLgdEkHomyKhMBIAwUIDPw_PBxyTZJJW54IWlaGudy9Wxpgi_EszuhuG0IJdC6ynNL-VBUwHZCGkiM8RmQOQdV3UKlcp0MozknS9lVrlenBnGjSK4eJxA1w5dQHssDVW6iK4Dm8SBFDE3B1EwBFd5ck9knoZpnr3e6Wqd2cXyGrPR2axEnjUhwaH0woZi9sXmBS6lbTTG7YfFhFStBqo6v0qvWW3eGyHE9Uzdef1x6WDTtX3TQd1OonX6wrRPv1ne75EhD8gbS3Zat41o264ivzTVbK_6X7htAAHrPbd1_nxoDgT4ezHlOtN1jOzqShCPuwgOaZCowGdbJtQZI2i9GlIAlwYIj-OTpTZGAeYpx35c7jGgIl5h1ALNpAguM-MEF4nnsJDb9j2rqOZkJjacMVORQQuHtUX7iogsdpLR-_dwIjca5H0aM5o0aIrQQ6prwVTZzpb8SoAQq9uFJXfSWO3JpTMo_3jH6oI4d8UpMXApy-QK9hQHTh8dNvM5uZmmirrpFBfTE409BvuYhcojjwXaT7KHKUNlG9hm6-WpPK2WrUD7PeTCHI_s6rW55Tb8bhhvmW0SEiGM2TN9ymrKZ_16GQlVmNiepsl9Ox4AROgYNqcuX95X9JbgwzhKuekfIfdY-c_C4EdY4AtkNl7X3idRNjJWZJwftUzkEXKj9bCvKeaBsZ4rkV-7yETAnNykoWnjAcS2uPpEro_ryr-H47AkXuxlDPGaZgLKAnutyEJz0nDjV2LCmk5M5ApqnjRS5c1jAQpypWIeUa1e6O0NyYVhkY95LLHAcWC_1YsRy63t51ntvl9POZKO7wlwu5UJ8z0QEfWb_4Schs2RX_bSEFgYY0QVWLXP-DoeFudYiaK2m1e661-LfTz07lwROQQ8xiDNuuiuOzdm1bTr4FIQxulQlxcjQJH0H1IwqhcydnkUWCa5-us65flEUhGay5YqI0hPN3XB675xm7_o2Xps3spFy6U0bWv737GUC39KboVDKIcHvwNS72udwy8XW5ggNEyyUkRvXj82LNo88sk38j_A-dYR0nKOKA1j9FktkA_EPJJ8gfkAGOKzDx4B8FA_yQ_yo3IFr4ge52EuKInVsyx7gmRJp4oaZdskQFHIs7eR3qs_TN_eLQohoY-mv4bi7E_Z-OjCi-DEPEjTXt-DgHRG_GOjzvdVYbee1ZJu1BHO2TY8uXAq_vxcno59VvFRVhfcThd9OyFl7Z7ckAG1fbpD5OD_WuGD_1CKYQcqW5PsfKyb2YVrBPKV_GtHLix2fFufH134RrSoBxUSBqcejnP_k7QsG-qrJjG8hDw7-_Us07L8gMbC-s72sd21LBgoQ9wawrrYHyd9Ml46eFF0UDMQXoo1cAp4xMYwhJhQ58FNrH0KuqoZedYlDjc6u9aTGRTZMGy6lvZlyl_sN9N9LDa1FQnKEh3cjNVPOmDflNLshX_Q4ctg0zEhk7TGkrhcvLszfda7-bDt79YdIrjGCEQTn9yjzHjWtL1PbgA8gbIAu0ZL6wjkYk6BH9d6esShFRHsp1anqAd2U9kwvltQnpUkOogPiHBz41__ZrMk2FLiX8F8Ztr_wAFlHmb1Bii3nyGTVj6bvEoXEenxy_UYCZIXtP9-3fR-xSAKQFbKVcZn0C4cY9W2BQOlVP0ZDYL7kDajI5yDXFQeT49WGyq3i_JuNYVb11aUEnP4ghWh2t2WKUsD1L4qS8uw6jQiHPt219V3wnstwoOa1e1y6I9zyT3FpGpg-SPm7LgyQ-JnyBwkwaU7YVkCeEAH8dJX6vgzECqUcNGXSFrpYyYBiPUh70rOs8CYA_FLcj3c5WvC0ChZU7DWbiz3vp4Rx9ApQm2ErI-78s631wINQalqItcEAVU6OLpvmESgrokmyhShciTTu2EM8OHvcQjxsO8A8NhjUw91uCNoU8MjF7gZnItrtuNJc1fKzMdYSeWvf6YrJbfPvR22naeTDlAqex7n-amuNDWDsf-ltV7E4t2-FQc5d8rBYMK9CTQtlVxzBc57OGa-R5cmOaTW8Zq9LzyH2hrEnONYzHWaqlx0m2W9Hiu7hVvQFqxlfsdlvTT4PmKsn0Y-ehiIpMN-3XEUruFzqCoGuU2nbdC08lGLnK5CKGIeYmcWH8InE_rIEf4lvxHpH3w0dn0tUJM094pdhNfniE-dZmPkQJvRjvEVLiQAYfakoacT1PsRfV2D4AxlC38q4Bol-SqpHpJQZ16Rf40cyucZXYzF_6mBFadHSrWqvk8VyhiLPaQVNGmuNoCue1_6qSAU4-d74ywXP_j_e5QWks4fbrXOBlh3ZqFnH_QEZVi-qNMESjgZ52GmjIqtY1IjPH0jZJNk5WXQRmEoMAwsPFT82QltTpopO8EswrsFLL_zuG2kD1tK3TVz_0GYiIGthiZp5ysIekpXgNi6Wd6Ura7nuD4MmwDzLfW8Qyn4ZzOdK94ylv91zEgUlyQu7qRyixvWjQ4Vuf0Jb6FaHb39bm_PL42belMXIerpqly8SOOxG48L9m43s85f51oAC7ZiSJj2Xp5-6oxjwCZ9LGm1yEkVOlg7MBH2WT1bzDDvdZgmXvEt0pbWPWP5SGH9Tmjr6kOQsgBFbj2fse2dvkr8DFTdvauUfT4bDpC5UmmWXNDaDKUmZuLSZPj1dWw6o1lB5_GF2XOJCOeIXZfc48a-jPO0p4PfqKGDoFK7bEDErnD1-0TvzkZu98w-z0Rd21g2qzjePx58e9RoSoX-xUS1OoUvJdrTKlk10YGh5YTRUE09GnLGsah9IWzlv4a6TE7o4QOK881Jks0nb3TVtt2Z8fuROndfIndlXBvHqAwu38va5mYn5UQdxHKTsW1KwsyiHEyygqN--v_6OSVMWAVD-IvZCSW6a69IIJKTtJTWwB-5_w1tDGl_vVlbx-ePlF2h_odqujQRvlAOGvqn_vFuuuUd9kD_MB7aUKegQa5IRs0SvOp43iBLpmYP6sht8qdHzLdluzFWx51VCYMzWp2cZjGhGyHN2UjHhkgFVoRPltwL0LLihC6heXdk4rmBdoezQe8SbzDvMOD0CCI_U0gad8GotaAUgsV95XQeBuUsd6OtpddsEFIgJRtcytH8qlZMt0eGTfe_Gcob6Ss8wzRNvwISINQ29AidZ4jYb2DUYQUF-4f4Eg0KT-Ohi_F4QcQ4OE6VnyDZkYGLrrDT6KaA44cFmmWeQjuglMMKGtZRszs77W0AiGDwelXHlNTUQrmgmJ9x5b9xdw20FcEkKP1JG7jCSomjcQqDbur1ZXf8djuKCFMk17hVXtpcsAdXLpKuly8wZwDeUXmy_vApouIllQoZplBMI1oZZFy8VS98W_vOow6mlXIgyORpYwDcWmTac5bSSI1riek-DXJiEfY4snpzkc-Wy3XLbsDTBr47Q86bjeHMVFgOPpoyYUwj_ueOzxH0fAJiZjww2tPyKIiKJz3-2GXWJG4DCneVE2rECGYTsQDqjxMLST6xZDDFajaB5uolj4khtZd8ufMW7rXlXd0Ec-2sfv51AZXzkSmJCwID4nE_rUpHNKaHPav4GE-P-yAqkpuutslnTm7ZirXm8UTc1GxZmOJ1GGT7gAgNLQGjnp9h6Cq8UT7lah9MZqy9Wy9ID5lHoWcgZYTcxH2fejlOxzEaT7wicbZRDW-bZXx_FN7PUr-b-XqqBjqCyu722Io2iKsvM7_MdQF1O75mujFpTFsnSHEUgYFqwsAfpktuUE_rVZuQ9vkfV0GBvAUvu-w6qPnl6B1QfWapmNzoyQBhlRdTklR5gZtxdo_6lCJtjveuoSa7JCh10mAyOy8mAy57lp74WvZaOXr4abn0xfF9K3bCQeIS8TrAP8U3J6Vmd-W7vaTAtRHc2t-VNxkpHDpgP6U7s4-keNGyuMB4_IXIoqOSfVRKZsFfH9Cdn11Umh2gSJl9V_n3TPoQhqk7w-H0bL7kUwHZ0sScOijLQDxrSH-ZGbDjR2P1qb-isbAgqnUXDo5EdfanA3f5rXYoAqocaOG0cwLCDVbUhRtwoEha35HIViPGa4ZzEh_5Wwr4FveqvsiazYUg-11qKNJYkmPjiD-jbo7AhHWxwGqdTMlSDn7nWveZ2s_35aqW8yHiY9QSzYUA-RwQF32VVTQOHwmAavMOrM1MMFR4isGV2yRZ06xoNn5QlZpgxFLXWY-yHHJ4lYK7gLvS_XN_QmQVZBhEjDRsoJndD8KmXS1OtzMrUpXlUx1QlFcdgfNzdtcPKlNlg1F7xTR9s8_pB2Dd1YH6sk_G2ekoGOmZFDXMOY0B20HPXjAXSbwPvMuCJ4VN7RbwS-nblKKNpLj5WSMYUbmCS6o_sH-YyWxcZ2RlaEWmAM1gwKsofgY0RKlT1h309khqzLSNyqN8h81mXN0VDR0ZlZAdbZyP3BUUrpX5PmEQLxAkjH5CQZbeprFqHWNaUTC4mRKKtSr40LXJljRwF6PIfLat3DE_JrS8gg0XF34CMsi-u_amMl5OyvpsHWbTYYPgoe9S0HEHT41Jz0ENUdkoITCd3TIwJCJ1Jx5-N24sngC9mUcwz_rdsDO136vrPcM4Le63VYkXWA2OacgTrQArPCUAsJyK5gEt-CJF9UZW7HvwBOkZYazbDg3Hw8-ws2T22qjcJzc-wKxGUl5ZRTKAYWotTnk9YZYeaHCrSugepzj4H5fOtV5jdoWrpPHiuBr39QysGd2ahkdxSi9p93mlbjyT5EZL53cI7EX8JXHoRq02SuriPjU6cUE4VPo5JPHmM1LZXc2m4n3LGTlbOyZctk5xaR9xwhe3YzIdgYByEWzvi_pVHwNHNB5PUJvPS2_32SqNI_PHpL1QmfxHoQq5-QDI0uM6XHXr6Fq5JFI04374cPjzRiWNSzDGs7SZ6NpDnMredX7oz7uyuL4D--AkEtc6pUUHnTStetpaHS66l6sNctC2M9ST6NFNMbMHFg8M9PH9bxmC7zy-Mk_iMlMw4W-ACiSFlptwnHkTBcaR0eZdSVtGenjF3qO9VkUuLVk1Y3GbCBxTB5PNHF0c3vBU5rtSG-HzbBA4RUyI7v7ca6QWtzfxOzdtAM8Va5gNCwqmaq8LKYa_gb8rgmcjaKAcJ4feMDGBaugBJ9bzb83q1SFk-4sExA8QEfQDHyc-SQG2AmGaalIJD_pC4QMPfkv03bcIcVmiyeS0zGeaVHzd76udmvxxsQus5k2FVxQEfXddB8T_4O5ZKB8ibL5yO0PL7SC-WuFqydXmLJ_i3szFtH3C5XAMgWW1OSa1EbdNmmvVOquo31DirlLeYXysQ6kIVe94_Es6_sK52hewhogagy0MH610sBzEUpfFTyVe_WNzABjq4Q0k8rEb1vPx3DknceDb-WJqW2-pNtEq0L8-ER_Xqrx8TE2JS7uJkG2GqdviT0KF0mKJt2dz9b8ZjOG6HTSUk3t0jwhA_FrBPwLJoLnwEE5Ynq-mwgsAxWVXUw_WZx--t3byxHefGmtIOQulbSjwJL7tfRXnY9vBHGMEV2EB8fYRDH4syiUNmHuedX91SAsy3q3joAlmNdND4PmsJ63dIRlyrBx_BLONlNuTO3uDk33hjloToQ9ssIDM_v1ylwWj60yvJHnbm28WXZiTuRtqjZMGQlkRLoxXVGFeTU0xXZaaCS_xI1LfOLh5khikjYuu_ZRyxTHwO_LdYx2Yut2nXtjzVPwgR87qJBHYugGpWi3BClAPv3Q__7Mz6onkNUL_HC7tmxF6AdnHz1fopaKIPCWyLht_PMf2JPp_fveBw0G1ybu25aK_yD_ieoJgMjomysdtmYiedFXHpqNPRXHtJ7XYaNQyMiQAvy-JBKdzw5uwSDgMqKwoIgRMFAV_Ybl0qjFLnv_C1E5Bfvf2jLUR_OFk8exU17--jk5LifIpRVd7so7AS-JNmzYf0-RQe8rWU7fEtKcN7zqCI_n6IE2J4COb6IoZk4V2nv9wyMGo4LqL9s_aoBfSbHe1WiZYG8B0OO1Jecjcr-98ZZEgnjOQApfdkLHzz0357Z3NqEOPpZejo17_urndLrvKMWKXOcVL7SDr_7VcYpq4am-jpNb7e4JdiqYsetYd6oWlFDwoCCW0gBUAkQ3NL466XvPcJLnmGMECdJuWwDYQ_N4ODIcwcHew03aetbKdK0TejnZ1ZEbgmOBYbuUyHPl0_USHw_gZH-YsA9gCWtAxgse4MkoiFMdyqWgvThROUHyDziyPG-bHjLpHwqgC6thSX0cLY1LEYQj3Xzab2z4qYo083roHpiB9QJnuuB65z4hRuyjqaQm7Ep81ms3Pqdq6czPhwOIKubovjbv-zCckJXS589v8UpxEJFUi4Q615dcaNUFcuTtkjpMeRYyNhuzHytjL5L7Zw4qsNdxhLtNnPT5KSi25eA-LQWGZOtg50fkWfPf-hyqTzpnUUr4dJsLUDvTDaNGloGP1H1HQHiBhODvKSirBJFZU2gs4v117KiJnF7y75_kVad33OcbAcK3a3svhxtfC9q62BiPAJVfb6s0_85Od88L7TBmlW4-iF0RDc6qeO-0ivmNCFukxxxze48TvsyqEU5DFWaSvwvRbRzOSt-xIqJFQH-fP1slcFooK4yHrNQdBDEF4LNKxJyOytMvELETE4M4Tn91I5C0zUs_JUftf85d6GJDvWWfVc19KsmAu0aLbd2jN6g7Z-TifhNyudDQ3NCiTPaouLNzcggzYoJaZNCtA_aOdlckk7X-wjBWGtw9TEtKNGfET5xktFdhA69_dV3GVfQ5I-7LkW4UaQ2gZ-h4eDAvX0YWZz6CISElmPw50IZPOz1P8-wS3a0AdusbJ2C2PMbyc0rogQdca9y3uPHJq4F57jKGLZeLZroaV5fyTmyfpG3Dxqc8Z8GUB6AX1q0Nme7pbRiLrForjfq5B8ESFsyR-xUMMMQ4kXhHGD2L9Ko-EdfqAUuQ49NsP0zsAiaU6EQ-Jvy1F9bzBsjc3Iv6gXh-l6hAej6CQxR3mefYA6zuUXEx-zz56G2DALVtxNY3UBoXOZ4MVsVBrKCz4ZG7FdbkdgYV9LWobPmQNkjyxgmb_yBj2ezF7WQkuBwFcKWZsR9wzkph0kvBFbaDjqWMg3E4cVTROiKO6RWP0rQI1Le2c-fRAKe4O2VVM9ZqBOHxA-9Lj01xIq-JaB4qjw-zfQTiyFvckKgMS7bY7Yct0DM0xjSNpwgm6OozGxogyOD28TSVg1C6MENnINRruX5utM3gIK5nFoQLE6cBWxuEzeMhJJSpETC73hEYm2EXTIzcfCQwCiO4lFooMCuk4w58Fd-AYwwtUUS9rGKLjC9xlklmegRct0akyEfhO_kHJ7cCp-P035Ld-EkLRBkxriFMN8j1b9os8pGFUI0oTajEdebghpYKP1fq77P7-5aX8pLC7D3sqozhHhOC4Ih8qoLOr_YEuKoepkvzEETO_hY3k4ZCZcGo_5O9czD98RLxf5wibfqKvom2LUIrv_HjPyDI840txzkewxSqaxIB9J9iqRddZwLNYuL2RLDqy8aTMgOpcgYYpQVgijU1qPUG59MoJG9STJVxRqi6XEg68Imh5kP_NApI3Il7p6zRzJWAbML73UrurbQENN1buc2QALcL_upeAXyWQKnD9rHsCxTZbszV1QQwQKq4RojO7OZQwLjCUjpRnoOG1OjDiBvosBmuv2XHlkB6rQHElbmSsKAoTXvvu-MQpUp74TAoqb0G3JqfZgDoYMzizrB_sxZJbJtAWy-L0Yy5Kp5u6o4H1eB1KfH4IhQVEnMlDElY7jgmyjgSnwN9bEndR7NcZBaUklQpW00mbpY1_oQjXn8dfw5PIs-zEwCh2mkCvQCn5AEYYhuKz9hjlN_RcoDpuWkNDoheypO2pk_9ign-wdzd7LjcgEfjUrRqSW3REFI63CEZXEHPi38SrYotxyhRdXkUiIt04Zt12G4hTqAx2NJ4m0Z_0SY9VulyzGlsGb7hPQtw2OXIR40zXXMcWhIfiScq8QPDRTxpw705OwNA_UAWFVPHgli8OJ3IYT15GqEP1MS9UhAfFzhHwr1lCURpra09IKeZwNu8PVv6uTOCxGnTK1EL4AUFBqNUJt0Kg6-uD1LxrHxNDGjU_RwqLwIOrzrr9_QUveV4XTS7-4C5Wl7aazREzopWe78tMQqJ9Z2SQZoAEKQirmnhtVAcsnv8EmHJUc7vOul1xzKC56ysyeM066v9VmY8lE-pMUmxuybfDGJUMCKENCQ1oeEazIA11j0vzX4cAar4A--qpBCJXYt6LkA9ycMVurz55KzsyxJimuCH1-SW5EahcZ-VlEZS6euYrAzUH8kPJRlyRFX22RVqidaLHLEtqEvsq7d4XJxCa72PcXvzFnU3TVwbVNiotjbSAPeoWF75DHGi9CnN6hy-yTMhVek2hXpne-Uxc5mK_zdYv29WiRX0ZQR8bizWeuHIumFsQTDuM0dHcr7Tf6-o1pBqYTv_LepCBErCARVCIRBNIMloalkMirDnfSMhfwOrR27tAzgjeMS81dBdJuuALUTlWrgzcXxcmK_4IL8ts0GaZ8A15jAWyhX1K0zJkB73D_2NiN0OsTY-cvJeotu1cD2mQ7gi45tCsWJ5fzskBxVOHzkaxRa6GUxVgPfvZX6pSjjx0vtbHyYs9_v1EqXLvCM_Kyf942dIsH09Zyg2vbDwDdGYbgq2Mo9wH30szj215fsZevU5vGhHnLqbBkNG9l9A_ufNUdLR2afWTC1WZtsjONua2A06kHN1OhSDr_DTia0dniKd9-JPsDHBwtCmI5ziRSszBCqhE5uY5c_yUmJsxRvPr6MoL1wGe3k8tXjztZFirRSuEnCFwtxWbaajP8ChW4P2uliG1WSNdjiLw-CP6Z2pBPXOkRyZPEi8rl_sg9aCFRLRB0JGT-E1_vYTnZlBUGgpQoQ2nF8_92zAuNPJPEsw4PF1l0cH9oQXUfSenw0kWsmxEW7X0xfM_WEKJuJPcVYfhzvAF0qkNjEdAexy9z0E313boRwZfC3D-iFjkjx1g_A-PKbcMddQSIn2XKc35FFKq_M2X87EpmLoJETajHYmbxV8AKL-ZvfU7fbuTcafEr61UOQB_6Xk6m77xzfDCifbZtpJFtrt_hL20HZjrKgtRbRjPl7StQONMmI0IzcEuFN_WkXq9mfIC8hUzPt1oAVu4BuU4Xuk2uoXz_ePdx_je_HC-nZPpFUz1_M5PVOOy5hFV3yjNquiOQ9RRgqiZ920K8SLJ0Ri-uzqAgJG2bBEUSgHedd4dGkfbhKUZrpDlpIEPKOW4Ltz6PCq4T2yll4A-RVpP1KA3peXhexZIJCwwJSmc1g_NzR9jrCFY61SWpvO1V_tTfKfLXMCgGutwsP4RBgi6fPp2H97OU5rFCA3NWTGy43WmWVysdYue9YWmgwde0wvWfOcgVrJZ2dxYYJZfWBeaiCCuORmrWplKytlcI9cCO_qEHvCyUh7aXnwB4Ffo3Tww2vrJTvZqhKRCnOOVdfTn01wXa5G4JgzxEwDxI0xjdMVedNlTHlU7z3Ix_KVVplvF01Md0Y6U54lbagJSCL3xjbMB4gFL92vDSgDXM816F1NTXJAEsQkJ5WO2bZRt5wNRSkaypMYSgw4_1rtCePLhsUuz4PUz48TmjkxqQs99_DxTeol1-4r42o_3-f74zZQINooNSZBrgaLLIjEsXKYjx7_tPjSByI1uwIWz1lHNP--aNo8c0TH29Mlb2AE5oWMeVTr5WDmE6djkCsna49FOFmXu3y3Vva1sFQEVPlbE1c_zL5ECllnrRbw3PEeIsNwpzbf2R53RQ1DSuegDTEqnENGH1PZ_fM_56oBSFpaV5TGMUgpwyClqIZjsXRx4IqFwYqN2sFS3u2fOAzGE58cu0puL3_IF_UTCWErvW3BM6PTl1icF4bF_DYwDeZMja3Y4eqHK-x5pud3hCVeDpg6rs-2ANrRx354DNSsmweHQgEMz_GUy36bfem7ABcr-YF2okVhT4SLXYtpN0hXYWm108VXia2CXvo6QZp0H2-JusAWjSSPJGo_QnpsYa3Taii_IBgy_1PpFuvzDeZ9QKTE-acckQEzMq4jr2-x09-6-JqFI61AU1XctnomxXgK_KOVbtHXIJQjFS8oEoddqSABi7IhxkwctNHsenm4zdic9UQcxa9UgXF4GtYmktrjKQt7WKdKUbxYn_QZzaC3NY3bubv3pF5UB6FDEOpbhehIjMaQa65zp38rFBJ9q3R12ZhnZHrReBqH0WqmKNLlmL1IU9sXaMXTFmntj2B0VJzt-7BIwRVkoX27ny5OharThw-qCGxM3LWPOhkkTIxhyRRCDwemwb3D4QVUX9XYkmo_FFUWxMUPyZENv89Q6600QvJvOMSg_5XwXbHJWQO59nsuouBS1Le9JLJ3-J3rPAI4b6HEyI3Rh8PkLrePD12VOtKIWTXxvct3qF4ZVf7sRrEqGkymmSTOlxxwD1oQuoXIa-ZlCRnlf86JbzEIef2de3y6fF9EpatQZYZgwtH5eu-b1MP2BfoAxKRdnsxaW2n-7uOlKYalB2r640oka0hyRK4rk1mUFq7x3sdOuXuVqh40fIaieL41CLZOEIi8SLk6ZViDZKPU4hm_67RzWSnKszJH0iGtwutbVf3gRhz_gyunhwHUMXJZJkBtO3CqUBS9OWx-HTFgtwpLpGkIBn7_SC9XmKMSge45mnhMy12YPdrQ_2RX7ei9UR4IU1b9uvfP-krLDbwvrSd3nQb2DDc6_PJ6mfGvUy_vab_SD9o0jWZbNFdmqP4gVAn_MSW-LwefvcrWS5uTNInmkcl22pzfLmhjpvzrUo1uI4uNTyAdjTPVrU7-EOLPYi7QxHBfzIE-NyE4qU_8c3ZEu3LSwHTAuQPwqbqgbWu-sDwzgiRtqOLtPCbJxHpkbLFabZ1kBCgfbDEmijXd2ZjHXr9_PKNAyqijYm_BvS1fUpfkp7DbqZjVsz0I4Aye88DSNE3lrZY_WPNXRr4Bw_DVjav3hKFebib4RkwNWSpwe74NNFVpniVTmmzNsSk2t8N12wIlQbK3jwa6DvrWU03cNvqKdNJYXpNgJf8nAGmkLFUUPekoWlJKLrm8FhwTbGPCoTvXz0KVR6jA3ZaM7iibKYJeJyuGgrG5G8uIyKd8_vxl_0-ixJ_Cqe0swITZOd0QWWkkCpVeuZh8TE5g5QTCqCjRC53xhP5fSRiokmYhlUyGMe7Oj0TCZnep9C4wGAKWyDejTPg_4EF77K4I2XunjKntumh9E6HbryOv3YVcbjmhX8GszPzrkzmRataaCrONmw4noQUjCASI3q56kbiFdx1j0yZL9mfQiiVNF8zo6h2EVLUeliiYnJX5C3S1cY-TVpWGlZxUt0ZxjJfXpOfciRgEfVEPPuTZENGp9cEEEaBdmkTEbBGfwzm_Z1INB9qdJXkKlTnxyDAKi6hCDF8-FaRG2h75aNKWoBqiqTMQCld9AmDLlgx1ajYl78ksnPdAaprQ_fgLbZVXE7H2qFW3FYMsTLOZI6UD07YqeByYBtzdvFZNsxGNw1t9gZ8wnw4qDHWQteWCxK-YcQpm2AXGk64FIbRHeLNblAHCfCsVrPPgf79DP-0tu2dv6Pe2DNifndz3jkaFAzVK0LCS9WMBBG_XwKzQ3Om7HqjW-5MUhqFRvy7j1aD-szAvhUFTG8qqA-VpQVocS75tWltzyOZ9JKaxfDrliTboYGxQ3dwBslkvePWyOSNXm_OzU2aTcbJBAIBocIhHXGRQLQzbjKnYl0rloEJeSSw_fkL-JtIhaCm_7tA5kZYd-1xYAPfly0PjZu6Wwly6jAPRfQt7y8b5L_mG0xovQXwW-0PCtXT_HP3QlvmGf5-INCc2FQNtTpYWPgdK2kIsHK3DPv0TvwCFNE_9fXpkPq-0VvvVdOIJYFhg_cd3zhWzdP11ofDYSVyvGVDWBv-JqIO2esZD5g8H2ixVkNCE1rZl1akOIz7Iu-_rAFzLFGQLVJNwDIrJXV2Si8WRK6zRLKAZFYYOUcB7GFypJo5ByJMZKIBPcfkrU8oK520nn_DgUlMhjx6vEyBVJPgsjlar7eTWfc8d04CvHX_GfQpl-AGnfTvmaWSW_q-g-kvTN7hJrQanbxAlzMU71Eezg4QVZ19VPqGB3Dx3I_oHB2gfRs07FLGW5E4TJ4CiY5rb806EjVnwM2NWeDMbdKMQYGZsBEvVwjoNE9nVWhGoTnx0k5KH-L6yU1zHf2zCv_1h-a6zKPHCJol4mYmtrAl0Kb69uk8G5ax7-TURt_0SWZdyxGTEUcKx0GFGd872ydT9sjh6bpQkdO0SZUUNC7j-z390sVvmM8DPOXPpZXDPN41gdzhdDpvVu3IZ9ZhqIgI4B6YwoO9bt4VBdeR7aXGOGD68dnrfGG-JHcmxM0f3uwAuf9VVvvZg9xRAxR3L1Gbz3h_0UhMZNIGgo_12LXb0itJzQT4RkcCrD5h6pAq23_1d91lPIXeiIr710rGjebQ0fH8Lgyaqb4tKqCQxApx6YKeZktAOQ6Qkhlxsuq7OBVAgGGwnaMZ0mL1NOCpEW9naoVhq2e3kjiIfkz0aNbO5vrUFtVrWZm24mPNdcO-ErDoirFHiOJ_TZ2exmi0Ce1NXPXIFY9_1knOuMGvFPOTI1GVUP3Mu0y9CYlvf68rlYu-ktpLTUvHi0ec0W1T50Rc6-e7A_ZU0U3dwHyFQz5ictEtV4jg0YhCRVvKZMA6tKYpPf8nckUrtnv8HnjcaRK-R6ICLC9CCVD90zt2oMDLZ1tKohgepjwCU7kKCKPl7OJM-o42xBHei1PRaXK4rA7TVtVQhqGflu4cKKQDTrsWy4UkYukdZQon-YGWamAzJHmAVscCrvSOPpa5vb7EOK9lDKsNBsFEHWget1FUkEAe93sHXUcO1EcdfQOYYb1MrLq4-Wg0o0LZPxkIZnRchAW5GZaTPWJ80oLMxOCJQ_lZLHI_sYPiFgJnWb6esrvXKufUrJSgBK-XvmKQbLbkqRVQE07RtQj8aG4oguksskG6De0egc0kM_sl5nO5EoaWjlnfkEkwTjxpvsxp_lMKE3Z2dQjaaW_v15kzTYAS2AOOhWzMlP6s3PEh9W57K8txoYdUwjhwkcXtsA2DkWEDitVTIvFHX2DvKIVAQ==
@@ -1,112 +0,0 @@
1
- from random import sample
2
- from .console import console
3
- from rich.table import Table
4
- from rich.text import Text
5
- from rich.panel import Panel
6
- from rich.layout import Layout
7
- from rich.padding import Padding
8
- from typer import Typer
9
-
10
- import textwrap
11
-
12
- app = Typer()
13
-
14
-
15
- def get_quotes():
16
- import json
17
- from pathlib import Path
18
-
19
- root = Path(__file__).parent
20
- if not (root / "quotes_decrypted.json").exists():
21
- from cryptography.fernet import Fernet
22
-
23
- key = console.input("[bold]Enter passkey:\n")
24
-
25
- with open(root / "quotes_encrypted.py", "rb") as f:
26
- encrypted = f.read()
27
-
28
- fernet = Fernet(key)
29
- decrypted = fernet.decrypt(encrypted)
30
-
31
- with open(root / "quotes_decrypted.json", "wt") as f:
32
- encrypted = f.write(decrypted.decode())
33
-
34
- payload = json.loads(decrypted.decode())
35
- return payload
36
-
37
- else:
38
- return json.load(open(root / "quotes_decrypted.json", "rt"))
39
-
40
-
41
- @app.callback(invoke_without_command=True)
42
- def print_random_quote():
43
-
44
- width = console.size.width
45
-
46
- if width < 12 + PADDING * 2:
47
- return
48
-
49
- quote = sample(QUOTES, 1)[0]
50
- index = QUOTES.index(quote)
51
-
52
- lines = [""]
53
-
54
- paragraphs = [""]
55
- for c in quote:
56
- if c == "\n":
57
- paragraphs.append("")
58
- else:
59
- paragraphs[-1] += c
60
-
61
- for i, paragraph in enumerate(paragraphs):
62
-
63
- if len(paragraph) == 0:
64
- lines.append("")
65
- continue
66
-
67
- if i == 0:
68
- first_padding = " " * PADDING
69
- else:
70
- first_padding = " " * (PADDING + 1)
71
-
72
- subsequent_padding = " " * (PADDING + 1)
73
-
74
- if i == 0:
75
- paragraph = f'"{paragraph}'
76
-
77
- if i + 1 == len(paragraphs):
78
- paragraph = f'{paragraph}"'
79
-
80
- lines += textwrap.wrap(
81
- paragraph,
82
- width=width - PADDING,
83
- replace_whitespace=False,
84
- initial_indent=first_padding,
85
- subsequent_indent=subsequent_padding,
86
- )
87
-
88
- lines.append("")
89
-
90
- longest = max(len(l) for l in lines)
91
- signoff = f"-CONFUCIUS #{index+1}"
92
- lines.append(" " * (longest - len(signoff)) + f"[italic]{signoff}[reset]")
93
-
94
- lines.append("")
95
-
96
- for line in lines:
97
- console.print(line, highlight=False)
98
-
99
-
100
- PADDING = 6
101
-
102
-
103
- def main():
104
- global QUOTES
105
- QUOTES = get_quotes()
106
- app()
107
-
108
-
109
- if __name__ == "__main__":
110
- global QUOTES
111
- QUOTES = get_quotes()
112
- app()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes