richqueue 1.8__tar.gz → 1.11__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.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: richqueue
3
+ Version: 1.11
4
+ Summary: RichQueue
5
+ Project-URL: Homepage, https://github.com/mwinokan/RichQueue/issues
6
+ Project-URL: Bug Tracker, https://github.com/mwinokan/RichQueue/issues
7
+ Author-email: Max Winokan <max@winokan.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: pandas
14
+ Requires-Dist: rich
15
+ Requires-Dist: typer
16
+ Description-Content-Type: text/markdown
17
+
18
+ # RichQueue
19
+
20
+ > 💰 RichQueue: A colourful and pythonic SLURM queue viewer
21
+
22
+ Installation from a modern python environment (>3.10) should be as simple as:
23
+
24
+ ```shell
25
+
26
+ pip install --upgrade richqueue
27
+ ```
28
+
29
+ To see your live-updating SLURM queue:
30
+
31
+ ```shell
32
+ rq
33
+ ```
34
+
35
+ ![queue_example](https://github.com/user-attachments/assets/d99ca5e9-7675-4853-ab28-2d7b4da855f2)
36
+
37
+ ## Other `rq` options
38
+
39
+ To see more detail:
40
+
41
+ ```rq --long```
42
+
43
+ To see someone else's queue:
44
+
45
+ ```rq --user USER```
46
+
47
+ To see the last `X` weeks history:
48
+
49
+ ```rq --hist X```
50
+
51
+ To see history for a given time period, e.g.:
52
+
53
+ ```rq --hist '3 days'```
54
+
55
+ To list available nodes on the cluster:
56
+
57
+ ```rq --idle```
58
+
59
+ To show a static view:
60
+
61
+ ```rq --no-loop```
62
+
63
+ ## Monitoring log files
64
+
65
+ If you keep your SLURM log files in a specific directory exported as the `LOGS` variable, you can use RichQueue to monitor results as they come in with
66
+
67
+ ```
68
+ res <JOB_ID>
69
+ ```
70
+
71
+ <img width="569" height="737" alt="Screenshot 2025-11-20 at 09 05 48" src="https://github.com/user-attachments/assets/1b0f6457-d769-4a1a-b331-bb7f121b5864" />
72
+
73
+ ## Changing to the working directory of a job
74
+
75
+ To change to the submission/working directory of a job:
76
+
77
+ ```
78
+ cd $(res --dir <JOB_ID>)
79
+ ```
80
+
81
+ You might find it convenient to add a shortcut to this to your login profile, e.g. `.bashrc_user` or `.bash_profile`:
82
+
83
+ ```
84
+ res --install-jd >> ~/.bashrc_user
85
+ source ~/.bashrc_user
86
+ ```
87
+
88
+ This will make the `jd` executable available. Change to the directory of the single active or most recently submitted job:
89
+
90
+ ```
91
+ jd
92
+ ```
93
+
94
+ Change to a specific job's directory:
95
+
96
+ ```
97
+ jd <JOB_ID>
98
+ ```
99
+
100
+ ## Submitting SLURM jobs
101
+
102
+ RichQueue also provides a convenient wrapper to the `sbatch` command which makes sure your log files end up in the directory specified by the `LOGS` variable, and prints a pretty summary of the submitted job. Just replace `sbatch` with `sb`. E.g.:
103
+
104
+ ```
105
+ sb --job-name test script.sh --script-arg1
106
+ ```
107
+
108
+ Would run this under the hood:
109
+
110
+ ```
111
+ sbatch --output=$LOGS/%j.log --error=$LOGS/%j.log --job-name test script.sh --script-arg1
112
+ ```
113
+
114
+ and display the following:
115
+
116
+ <img width="474" height="255" alt="Screenshot 2025-11-20 at 10 37 19" src="https://github.com/user-attachments/assets/4464f813-841a-45bd-864e-00dd3dabf924" />
@@ -0,0 +1,99 @@
1
+ # RichQueue
2
+
3
+ > 💰 RichQueue: A colourful and pythonic SLURM queue viewer
4
+
5
+ Installation from a modern python environment (>3.10) should be as simple as:
6
+
7
+ ```shell
8
+
9
+ pip install --upgrade richqueue
10
+ ```
11
+
12
+ To see your live-updating SLURM queue:
13
+
14
+ ```shell
15
+ rq
16
+ ```
17
+
18
+ ![queue_example](https://github.com/user-attachments/assets/d99ca5e9-7675-4853-ab28-2d7b4da855f2)
19
+
20
+ ## Other `rq` options
21
+
22
+ To see more detail:
23
+
24
+ ```rq --long```
25
+
26
+ To see someone else's queue:
27
+
28
+ ```rq --user USER```
29
+
30
+ To see the last `X` weeks history:
31
+
32
+ ```rq --hist X```
33
+
34
+ To see history for a given time period, e.g.:
35
+
36
+ ```rq --hist '3 days'```
37
+
38
+ To list available nodes on the cluster:
39
+
40
+ ```rq --idle```
41
+
42
+ To show a static view:
43
+
44
+ ```rq --no-loop```
45
+
46
+ ## Monitoring log files
47
+
48
+ If you keep your SLURM log files in a specific directory exported as the `LOGS` variable, you can use RichQueue to monitor results as they come in with
49
+
50
+ ```
51
+ res <JOB_ID>
52
+ ```
53
+
54
+ <img width="569" height="737" alt="Screenshot 2025-11-20 at 09 05 48" src="https://github.com/user-attachments/assets/1b0f6457-d769-4a1a-b331-bb7f121b5864" />
55
+
56
+ ## Changing to the working directory of a job
57
+
58
+ To change to the submission/working directory of a job:
59
+
60
+ ```
61
+ cd $(res --dir <JOB_ID>)
62
+ ```
63
+
64
+ You might find it convenient to add a shortcut to this to your login profile, e.g. `.bashrc_user` or `.bash_profile`:
65
+
66
+ ```
67
+ res --install-jd >> ~/.bashrc_user
68
+ source ~/.bashrc_user
69
+ ```
70
+
71
+ This will make the `jd` executable available. Change to the directory of the single active or most recently submitted job:
72
+
73
+ ```
74
+ jd
75
+ ```
76
+
77
+ Change to a specific job's directory:
78
+
79
+ ```
80
+ jd <JOB_ID>
81
+ ```
82
+
83
+ ## Submitting SLURM jobs
84
+
85
+ RichQueue also provides a convenient wrapper to the `sbatch` command which makes sure your log files end up in the directory specified by the `LOGS` variable, and prints a pretty summary of the submitted job. Just replace `sbatch` with `sb`. E.g.:
86
+
87
+ ```
88
+ sb --job-name test script.sh --script-arg1
89
+ ```
90
+
91
+ Would run this under the hood:
92
+
93
+ ```
94
+ sbatch --output=$LOGS/%j.log --error=$LOGS/%j.log --job-name test script.sh --script-arg1
95
+ ```
96
+
97
+ and display the following:
98
+
99
+ <img width="474" height="255" alt="Screenshot 2025-11-20 at 10 37 19" src="https://github.com/user-attachments/assets/4464f813-841a-45bd-864e-00dd3dabf924" />
@@ -3,7 +3,7 @@ requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
  [project]
5
5
  name = "richqueue"
6
- version = "1.8"
6
+ version = "1.11"
7
7
  authors = [
8
8
  { name = "Max Winokan", email = "max@winokan.com" },
9
9
  ]
@@ -32,4 +32,4 @@ include = [
32
32
  [project.scripts]
33
33
  rq = "richqueue.main:main"
34
34
  res = "richqueue.log:main"
35
- confucius = "richqueue.wisdom:main"
35
+ sb = "richqueue.submit:main"
@@ -0,0 +1,59 @@
1
+ from .slurm import combined_df
2
+
3
+
4
+ def guess_current_job(user: str, **kwargs):
5
+
6
+ global JOB_DF
7
+ JOB_DF = combined_df(user=user, **kwargs)
8
+
9
+ assert len(JOB_DF), "No jobs found"
10
+
11
+ pending = JOB_DF[JOB_DF["job_state"] == "PENDING"]
12
+ running = JOB_DF[JOB_DF["job_state"] == "RUNNING"]
13
+ history = JOB_DF[~JOB_DF["job_state"].isin(["PENDING", "RUNNING"])]
14
+
15
+ # pick the single active job
16
+ if len(running) == 1:
17
+ return running.iloc[0]
18
+
19
+ # pick the most recently submitted active job
20
+ elif len(running) > 1:
21
+ return running.sort_values(by="submit_time", ascending=False).iloc[0]
22
+
23
+ # pick the single pending job
24
+ elif len(pending) == 1:
25
+ return pending.iloc[0]
26
+
27
+ # pick the most recently submitted pending job
28
+ elif len(pending) > 1:
29
+ return pending.sort_values(by="submit_time", ascending=False).iloc[0]
30
+
31
+ # pick the most recently submitted pending job
32
+ else:
33
+ return pending.sort_values(by="submit_time", ascending=False).iloc[0]
34
+
35
+ return job
36
+
37
+
38
+ def get_specific_job(job: int, user: str, **kwargs):
39
+
40
+ global JOB_DF
41
+ JOB_DF = combined_df(user=user, **kwargs)
42
+
43
+ assert len(JOB_DF), "No jobs found"
44
+
45
+ matches = JOB_DF[JOB_DF["job_id"] == job]
46
+
47
+ if not len(matches):
48
+ console.print("[red bold]Could not find job with ID {job_id=}")
49
+ return None
50
+
51
+ elif len(matches) > 1:
52
+
53
+ matches = matches[matches["command"].notna()]
54
+
55
+ if len(matches) > 1:
56
+ console.print(matches.to_dict(orient="records"))
57
+ raise ValueError("Multiple job matches")
58
+
59
+ return matches.iloc[0]
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import time
2
3
 
3
4
  from rich.live import Live
@@ -9,9 +10,10 @@ from typer import Typer, Argument, Option
9
10
 
10
11
  from .console import console
11
12
  from .layout import dual_layout
12
- from .slurm import combined_df, get_user, PANEL_PADDING
13
+ from .slurm import get_user, PANEL_PADDING
13
14
  from .table import job_table, log_table
14
15
  from .tools import curry
16
+ from .job import guess_current_job, get_specific_job
15
17
 
16
18
  # access logs
17
19
 
@@ -31,12 +33,27 @@ def show_log(
31
33
  help="Query jobs for another user",
32
34
  ),
33
35
  ] = None,
34
- long: Annotated[bool, Option("-v", "--long", help="More detailed output")] = False,
35
- wisdom: Annotated[
36
- bool, Option("-w", "--wisdom", help="Show a confucius quote on exit")
36
+ cwd: Annotated[
37
+ bool,
38
+ Option("-d", "--dir", help="Print job's current working directory and exit"),
39
+ ] = False,
40
+ install_jd: Annotated[
41
+ bool, Option("--install-jd", help="Print function to create jd wrapper")
37
42
  ] = False,
43
+ long: Annotated[bool, Option("-v", "--long", help="More detailed output")] = False,
38
44
  ):
39
45
 
46
+ if install_jd:
47
+ print(
48
+ """
49
+ jd() {
50
+ target="$(res --dir "$@")"
51
+ cd "$target"
52
+ }
53
+ """
54
+ )
55
+ sys.exit(0)
56
+
40
57
  if user is None:
41
58
  user = get_user()
42
59
 
@@ -55,8 +72,19 @@ def show_log(
55
72
  job = job_getter()
56
73
 
57
74
  if job is None:
75
+ print("Could not get job")
58
76
  return None
59
77
 
78
+ if cwd:
79
+ cwd = job.get("current_working_directory")
80
+
81
+ if not cwd:
82
+ print("Could not get job's 'current_working_directory'")
83
+ sys.exit(1)
84
+
85
+ print(cwd)
86
+ sys.exit(0)
87
+
60
88
  layout_func = curry(dual_layout, log_layout_pair)
61
89
 
62
90
  layout = layout_func(job_getter=job_getter, long=long)
@@ -76,10 +104,6 @@ def show_log(
76
104
  time.sleep(1)
77
105
  except KeyboardInterrupt:
78
106
  live.stop()
79
- if wisdom:
80
- from .wisdom import print_random_quote
81
-
82
- print_random_quote()
83
107
 
84
108
 
85
109
  def log_layout_pair(job_getter, **kwargs):
@@ -102,64 +126,6 @@ def log_layout_pair(job_getter, **kwargs):
102
126
  return upper, lower
103
127
 
104
128
 
105
- def guess_current_job(user: str, **kwargs):
106
-
107
- global JOB_DF
108
- JOB_DF = combined_df(user=user, **kwargs)
109
-
110
- assert len(JOB_DF), "No jobs found"
111
-
112
- pending = JOB_DF[JOB_DF["job_state"] == "PENDING"]
113
- running = JOB_DF[JOB_DF["job_state"] == "RUNNING"]
114
- history = JOB_DF[~JOB_DF["job_state"].isin(["PENDING", "RUNNING"])]
115
-
116
- # pick the single active job
117
- if len(running) == 1:
118
- return running.iloc[0]
119
-
120
- # pick the most recently submitted active job
121
- elif len(running) > 1:
122
- return running.sort_values(by="submit_time", ascending=False).iloc[0]
123
-
124
- # pick the single pending job
125
- elif len(pending) == 1:
126
- return pending.iloc[0]
127
-
128
- # pick the most recently submitted pending job
129
- elif len(pending) > 1:
130
- return pending.sort_values(by="submit_time", ascending=False).iloc[0]
131
-
132
- # pick the most recently submitted pending job
133
- else:
134
- return pending.sort_values(by="submit_time", ascending=False).iloc[0]
135
-
136
- return job
137
-
138
-
139
- def get_specific_job(job: int, user: str, **kwargs):
140
-
141
- global JOB_DF
142
- JOB_DF = combined_df(user=user, **kwargs)
143
-
144
- assert len(JOB_DF), "No jobs found"
145
-
146
- matches = JOB_DF[JOB_DF["job_id"] == job]
147
-
148
- if not len(matches):
149
- console.print("[red bold]Could not find job with ID {job_id=}")
150
- return None
151
-
152
- elif len(matches) > 1:
153
-
154
- matches = matches[matches["command"].notna()]
155
-
156
- if len(matches) > 1:
157
- console.print(matches.to_dict(orient="records"))
158
- raise ValueError("Multiple job matches")
159
-
160
- return matches.iloc[0]
161
-
162
-
163
129
  def main():
164
130
  app()
165
131
 
@@ -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:
@@ -123,19 +123,20 @@ def show(
123
123
  console.print(f"[red bold]{e}[reset]")
124
124
 
125
125
 
126
- @app.command()
127
- def log(job: Annotated[int, Option(help="Show logs from a specific job")]):
128
- raise NotImplementedError
126
+ def parse_history_string(hist: str, unit: str = "week") -> (int, str):
127
+ """Parse one or two part string into integer value and string unit"""
128
+
129
+ hist = " ".join(hist)
130
+ hist = hist.split()
129
131
 
132
+ assert len(hist) <= 2, "wrong number of 'hist' values"
130
133
 
131
- @app.command()
132
- def dir(job: Annotated[int, Option(help="Get job directory")]):
133
- raise NotImplementedError
134
+ if len(hist) == 2:
135
+ unit = hist[1]
134
136
 
137
+ hist = int(hist[0])
135
138
 
136
- @app.command()
137
- def nickname():
138
- raise NotImplementedError
139
+ return hist, unit
139
140
 
140
141
 
141
142
  # start Typer app
@@ -0,0 +1,54 @@
1
+ import sys
2
+ import subprocess
3
+ from os import environ
4
+ from pathlib import Path
5
+
6
+ from .main import show
7
+ from .console import console
8
+
9
+
10
+ def submit():
11
+
12
+ log_dir = environ.get("LOGS")
13
+
14
+ if not log_dir:
15
+ console.print("[red bold]LOGS variable not set")
16
+ sys.exit(1)
17
+
18
+ log_dir = Path(log_dir)
19
+
20
+ sbatch_args = sys.argv[1:]
21
+
22
+ commands = [
23
+ "sbatch",
24
+ "--output=" f"{log_dir.resolve()}/%j.log",
25
+ "--error=" f"{log_dir.resolve()}/%j.log",
26
+ *sbatch_args,
27
+ ]
28
+
29
+ print(" ".join(commands))
30
+
31
+ x = subprocess.run(
32
+ commands, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE
33
+ )
34
+
35
+ if x.returncode != 0:
36
+ console.print(f"[red bold]{x.stderr.decode().strip()}")
37
+ sys.exit(1)
38
+
39
+ message = x.stdout.decode().strip()
40
+
41
+ job_id = int(message.split()[-1])
42
+
43
+ console.print(f"[bold green]{message}:")
44
+
45
+ show(job=job_id, no_loop=True, hist=None)
46
+ sys.exit(0)
47
+
48
+
49
+ def main():
50
+ submit()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
richqueue-1.8/PKG-INFO DELETED
@@ -1,57 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: richqueue
3
- Version: 1.8
4
- Summary: RichQueue
5
- Project-URL: Homepage, https://github.com/mwinokan/RichQueue/issues
6
- Project-URL: Bug Tracker, https://github.com/mwinokan/RichQueue/issues
7
- Author-email: Max Winokan <max@winokan.com>
8
- License-File: LICENSE
9
- Classifier: License :: OSI Approved :: MIT License
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.10
13
- Requires-Dist: pandas
14
- Requires-Dist: rich
15
- Requires-Dist: typer
16
- Description-Content-Type: text/markdown
17
-
18
- # RichQueue
19
-
20
- > 💰 RichQueue: A colourful and pythonic SLURM queue viewer
21
-
22
- Installation from a modern python environment (>3.10) should be as simple as:
23
-
24
- ```shell
25
-
26
- pip install --upgrade richqueue
27
- ```
28
-
29
- To see your live-updating SLURM queue:
30
-
31
- ```shell
32
- rq
33
- ```
34
-
35
- ![queue_example](https://github.com/user-attachments/assets/d99ca5e9-7675-4853-ab28-2d7b4da855f2)
36
-
37
- ## Other options
38
-
39
- To see more detail:
40
-
41
- ```rq --long```
42
-
43
- To see someone else's queue:
44
-
45
- ```rq --user USER```
46
-
47
- To see the last `X` weeks history:
48
-
49
- ```rq --hist X```
50
-
51
- To list available nodes on the cluster:
52
-
53
- ```rq --idle```
54
-
55
- To show a static view:
56
-
57
- ```rq --no-loop```
richqueue-1.8/README.md DELETED
@@ -1,40 +0,0 @@
1
- # RichQueue
2
-
3
- > 💰 RichQueue: A colourful and pythonic SLURM queue viewer
4
-
5
- Installation from a modern python environment (>3.10) should be as simple as:
6
-
7
- ```shell
8
-
9
- pip install --upgrade richqueue
10
- ```
11
-
12
- To see your live-updating SLURM queue:
13
-
14
- ```shell
15
- rq
16
- ```
17
-
18
- ![queue_example](https://github.com/user-attachments/assets/d99ca5e9-7675-4853-ab28-2d7b4da855f2)
19
-
20
- ## Other options
21
-
22
- To see more detail:
23
-
24
- ```rq --long```
25
-
26
- To see someone else's queue:
27
-
28
- ```rq --user USER```
29
-
30
- To see the last `X` weeks history:
31
-
32
- ```rq --hist X```
33
-
34
- To list available nodes on the cluster:
35
-
36
- ```rq --idle```
37
-
38
- To show a static view:
39
-
40
- ```rq --no-loop```
@@ -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
File without changes