ladok3 4.10__py3-none-any.whl → 5.4__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.
ladok3/cli.py CHANGED
@@ -26,92 +26,131 @@ import ladok3.student
26
26
 
27
27
  dirs = appdirs.AppDirs("ladok", "dbosk@kth.se")
28
28
 
29
+
29
30
  def err(rc, msg):
30
- print(f"{sys.argv[0]}: error: {msg}", file=sys.stderr)
31
- sys.exit(rc)
31
+ """Print error message to stderr and exit with given return code.
32
+
33
+ Args:
34
+ rc (int): Return code to exit with.
35
+ msg (str): Error message to display.
36
+ """
37
+ print(f"{sys.argv[0]}: error: {msg}", file=sys.stderr)
38
+ sys.exit(rc)
39
+
32
40
 
33
41
  def warn(msg):
34
- print(f"{sys.argv[0]}: {msg}", file=sys.stderr)
42
+ """Print warning message to stderr.
43
+
44
+ Args:
45
+ msg (str): Warning message to display.
46
+ """
47
+ print(f"{sys.argv[0]}: {msg}", file=sys.stderr)
48
+
49
+
35
50
  def store_ladok_session(ls, credentials):
36
- if not os.path.isdir(dirs.user_cache_dir):
37
- os.makedirs(dirs.user_cache_dir)
51
+ """Store a LadokSession object to disk with encryption.
38
52
 
39
- file_path = dirs.user_cache_dir + "/LadokSession"
53
+ Saves the session object as an encrypted pickle file in the user's cache directory.
54
+ The credentials are used to derive an encryption key for security.
40
55
 
41
- pickled_ls = pickle.dumps(ls)
42
- if not credentials or len(credentials) < 2:
43
- raise ValueError(f"Missing credentials, see `ladok login -h`.")
56
+ Args:
57
+ ls (LadokSession): The session object to store.
58
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
44
59
 
45
- if isinstance(credentials, dict):
46
- try:
47
- salt = credentials["username"]
48
- passwd = credentials["password"]
49
- except KeyError:
50
- credentials = list(credentials.values())
51
- salt = credentials[0]
52
- passwd = credentials[1]
53
- else:
54
- salt = credentials[0]
55
- passwd = credentials[1]
56
-
57
- kdf = PBKDF2HMAC(
58
- algorithm=hashes.SHA256(),
59
- length=32,
60
- salt=salt.encode("utf-8"),
61
- iterations=100000
62
- )
63
- key = base64.urlsafe_b64encode(kdf.derive(passwd.encode("utf-8")))
64
-
65
-
66
- fernet_protocol = Fernet(key)
67
- encrypted_ls = fernet_protocol.encrypt(pickled_ls)
68
-
69
-
70
- with open(file_path, "wb") as file:
71
- file.write(encrypted_ls)
72
-
73
- def restore_ladok_session(credentials):
74
- file_path = dirs.user_cache_dir + "/LadokSession"
60
+ Raises:
61
+ ValueError: If credentials are missing or invalid.
62
+ """
63
+ if not os.path.isdir(dirs.user_cache_dir):
64
+ os.makedirs(dirs.user_cache_dir)
75
65
 
76
- if os.path.isfile(file_path):
77
- with open(file_path, "rb") as file:
78
- encrypted_ls = file.read()
79
- if not credentials or len(credentials) < 2:
66
+ file_path = dirs.user_cache_dir + "/LadokSession"
67
+
68
+ pickled_ls = pickle.dumps(ls)
69
+ if not credentials or len(credentials) < 2:
80
70
  raise ValueError(f"Missing credentials, see `ladok login -h`.")
81
71
 
82
- if isinstance(credentials, dict):
72
+ if isinstance(credentials, dict):
83
73
  try:
84
- salt = credentials["username"]
85
- passwd = credentials["password"]
74
+ salt = credentials["username"]
75
+ passwd = credentials["password"]
86
76
  except KeyError:
87
- credentials = list(credentials.values())
88
- salt = credentials[0]
89
- passwd = credentials[1]
90
- else:
77
+ credentials = list(credentials.values())
78
+ salt = credentials[0]
79
+ passwd = credentials[1]
80
+ else:
91
81
  salt = credentials[0]
92
82
  passwd = credentials[1]
93
83
 
94
- kdf = PBKDF2HMAC(
84
+ kdf = PBKDF2HMAC(
95
85
  algorithm=hashes.SHA256(),
96
86
  length=32,
97
87
  salt=salt.encode("utf-8"),
98
- iterations=100000
99
- )
100
- key = base64.urlsafe_b64encode(kdf.derive(passwd.encode("utf-8")))
88
+ iterations=100000,
89
+ )
90
+ key = base64.urlsafe_b64encode(kdf.derive(passwd.encode("utf-8")))
101
91
 
92
+ fernet_protocol = Fernet(key)
93
+ encrypted_ls = fernet_protocol.encrypt(pickled_ls)
94
+
95
+ with open(file_path, "wb") as file:
96
+ file.write(encrypted_ls)
97
+
98
+
99
+ def restore_ladok_session(credentials):
100
+ """Restore a LadokSession object from disk.
101
+
102
+ Attempts to load and decrypt a previously stored session object. Returns None
103
+ if no cached session exists or decryption fails.
104
+
105
+ Args:
106
+ credentials (tuple): Tuple of (institution, vars) used for key derivation.
107
+
108
+ Returns:
109
+ LadokSession or None: The restored session object, or None if unavailable.
110
+ """
111
+ file_path = dirs.user_cache_dir + "/LadokSession"
112
+
113
+ if os.path.isfile(file_path):
114
+ with open(file_path, "rb") as file:
115
+ encrypted_ls = file.read()
116
+ if not credentials or len(credentials) < 2:
117
+ raise ValueError(f"Missing credentials, see `ladok login -h`.")
118
+
119
+ if isinstance(credentials, dict):
120
+ try:
121
+ salt = credentials["username"]
122
+ passwd = credentials["password"]
123
+ except KeyError:
124
+ credentials = list(credentials.values())
125
+ salt = credentials[0]
126
+ passwd = credentials[1]
127
+ else:
128
+ salt = credentials[0]
129
+ passwd = credentials[1]
130
+
131
+ kdf = PBKDF2HMAC(
132
+ algorithm=hashes.SHA256(),
133
+ length=32,
134
+ salt=salt.encode("utf-8"),
135
+ iterations=100000,
136
+ )
137
+ key = base64.urlsafe_b64encode(kdf.derive(passwd.encode("utf-8")))
138
+
139
+ fernet_protocol = Fernet(key)
140
+ try:
141
+ pickled_ls = fernet_protocol.decrypt(encrypted_ls)
142
+ except Exception as err:
143
+ warn(f"cache was corrupted, cannot decrypt: {err}")
144
+ pickled_ls = None
145
+ if pickled_ls:
146
+ return pickle.loads(pickled_ls)
147
+
148
+ return None
102
149
 
103
- fernet_protocol = Fernet(key)
104
- try:
105
- pickled_ls = fernet_protocol.decrypt(encrypted_ls)
106
- except Exception as err:
107
- warn(f"cache was corrupted, cannot decrypt: {err}")
108
- pickled_ls = None
109
- if pickled_ls:
110
- return pickle.loads(pickled_ls)
111
150
 
112
- return None
113
151
  def update_credentials_in_keyring(ls, args):
114
- print("""
152
+ print(
153
+ """
115
154
  This login process is exactly the same as when you log in using
116
155
  the web browser. You need three things:
117
156
 
@@ -126,57 +165,66 @@ the web browser. You need three things:
126
165
 
127
166
  3) Your password at your institution.
128
167
 
129
- """)
130
- while True:
131
- institution = input("Institution: ")
132
- matches = sa.find_entity_data_by_name(institution)
168
+ """
169
+ )
170
+ while True:
171
+ institution = input("Institution: ")
172
+ matches = sa.find_entity_data_by_name(institution)
133
173
 
134
- if not matches:
135
- print("No match, try again.")
136
- continue
174
+ if not matches:
175
+ print("No match, try again.")
176
+ continue
137
177
 
138
- if len(matches) > 1:
139
- print("More than one match. Which one?")
140
- for match in matches:
141
- print(f"- {match['title']}")
142
- continue
178
+ if len(matches) > 1:
179
+ print("More than one match. Which one?")
180
+ for match in matches:
181
+ print(f"- {match['title']}")
182
+ continue
143
183
 
144
- match = matches[0]
184
+ match = matches[0]
145
185
 
146
- print(f"Matched uniquely, using {match['title']}\n"
147
- f" with domain {match['domain']} and\n"
148
- f" unique identifier {match['id']}.")
186
+ print(
187
+ f"Matched uniquely, using {match['title']}\n"
188
+ f" with domain {match['domain']} and\n"
189
+ f" unique identifier {match['id']}."
190
+ )
149
191
 
150
- institution = match['id']
151
- break
192
+ institution = match["id"]
193
+ break
152
194
 
153
- vars = {
154
- "username": input("Institution username: "),
155
- "password": getpass.getpass("Institution password: [input is hidden] ")
156
- }
157
- while True:
158
- temp_ls = ladok3.LadokSession(institution, vars=vars)
195
+ vars = {
196
+ "username": input("Institution username: "),
197
+ "password": getpass.getpass("Institution password: [input is hidden] "),
198
+ }
199
+ while True:
200
+ temp_ls = ladok3.LadokSession(institution, vars=vars)
201
+
202
+ try:
203
+ temp_ls.user_info_JSON()
204
+ except weblogin.AuthenticationError as err:
205
+ adjust_vars(vars, err.variables)
206
+ else:
207
+ break
159
208
 
160
209
  try:
161
- temp_ls.user_info_JSON()
162
- except weblogin.AuthenticationError as err:
163
- adjust_vars(vars, err.variables)
164
- else:
165
- break
166
-
167
- try:
168
- keyring.set_password("ladok3", "institution", institution)
169
- keyring.set_password("ladok3", "vars", ";".join(vars.keys()))
170
- for key, value in vars.items():
171
- keyring.set_password("ladok3", key, value)
172
- except Exception as err:
173
- globals()["err"](-1, f"You don't seem to have a working keyring. "
174
- f"Use one of the other methods, see "
175
- f"`ladok login -h`: {err}.")
176
-
177
- clear_cache(ls, args)
210
+ keyring.set_password("ladok3", "institution", institution)
211
+ keyring.set_password("ladok3", "vars", ";".join(vars.keys()))
212
+ for key, value in vars.items():
213
+ keyring.set_password("ladok3", key, value)
214
+ except Exception as err:
215
+ globals()["err"](
216
+ -1,
217
+ f"You don't seem to have a working keyring. "
218
+ f"Use one of the other methods, see "
219
+ f"`ladok login -h`: {err}.",
220
+ )
221
+
222
+ clear_cache(ls, args)
223
+
224
+
178
225
  def adjust_vars(vars, form_variables):
179
- print("""
226
+ print(
227
+ """
180
228
  Some part of the authentication went wrong. Either you typed your username or
181
229
  password incorrectly, or your institution requires some adjustments. We'll
182
230
  guide you through it.
@@ -191,125 +239,139 @@ when it should be 'dbosk@ug.kth.se' --- or something similar. Use your
191
239
  institution's login page to figure this out.
192
240
 
193
241
  Note: Your password will be visible on screen during this process.
194
- """)
195
- input("\nPress return to continue.\n")
196
-
197
- for key, value in form_variables.items():
198
- key = key.casefold()
199
- new_val = input(f"{key} = '{value}' "
200
- f"[enter new value, blank to keep] ")
201
- if new_val:
202
- vars[key] = new_val
242
+ """
243
+ )
244
+ input("\nPress return to continue.\n")
245
+
246
+ for key, value in form_variables.items():
247
+ key = key.casefold()
248
+ new_val = input(f"{key} = '{value}' " f"[enter new value, blank to keep] ")
249
+ if new_val:
250
+ vars[key] = new_val
251
+
252
+
203
253
  def load_credentials(filename="config.json"):
204
- """
205
- Loads credentials from environment or file named filename.
206
- Returns the tuple (instituation, credential dictionary) that
207
- can be passed to `LadokSession(instiution, credential dictionary)`.
208
- """
209
-
210
- try:
211
- institution = keyring.get_password("ladok3", "institution")
212
- vars_keys = keyring.get_password("ladok3", "vars")
213
-
214
- vars = {}
215
- for key in vars_keys.split(";"):
216
- vars[key] = keyring.get_password("ladok3", key)
217
-
218
- if institution and vars:
219
- return institution, vars
220
- except:
221
- pass
222
- try:
223
- institution = "KTH Royal Institute of Technology"
224
- vars = {
225
- "username": keyring.get_password("ladok3", "username"),
226
- "password": keyring.get_password("ladok3", "password")
227
- }
228
- if vars:
229
- return institution, vars
230
- except:
231
- pass
232
- try:
233
- institution = os.environ["LADOK_INST"]
234
- except:
235
- institution = "KTH Royal Institute of Technology"
236
-
237
- try:
238
- vars = {
239
- "username": os.environ["LADOK_USER"],
240
- "password": os.environ["LADOK_PASS"]
241
- }
242
- if institution and vars:
243
- return institution, vars
244
- except:
245
- pass
246
- try:
247
- vars_keys = os.environ["LADOK_VARS"]
248
-
249
- vars = {}
250
- for key in vars_keys.split(":"):
251
- vars[key] = os.environ[key]
252
-
253
- if institution and vars:
254
- return institution, vars
255
- except:
256
- pass
257
- try:
258
- with open(filename) as conf_file:
259
- config = json.load(conf_file)
260
-
261
- institution = config.pop("institution",
262
- "KTH Royal Institute of Technology")
263
- return institution, config
264
- except:
265
- pass
266
-
267
- return None, None
254
+ """
255
+ Loads credentials from environment or file named filename.
256
+ Returns the tuple (instituation, credential dictionary) that
257
+ can be passed to `LadokSession(instiution, credential dictionary)`.
258
+ """
259
+
260
+ try:
261
+ institution = os.environ["LADOK_INST"]
262
+ except:
263
+ institution = "KTH Royal Institute of Technology"
264
+ try:
265
+ vars = {
266
+ "username": os.environ["LADOK_USER"],
267
+ "password": os.environ["LADOK_PASS"],
268
+ }
269
+ if institution and vars["username"] and vars["password"]:
270
+ return institution, vars
271
+ except:
272
+ pass
273
+ try:
274
+ vars_keys = os.environ["LADOK_VARS"]
275
+
276
+ vars = {}
277
+ for key in vars_keys.split(":"):
278
+ try:
279
+ value = os.environ[key]
280
+ if value:
281
+ vars[key] = value
282
+ except KeyError:
283
+ warn(f"Variable {key} not set, ignoring.")
284
+
285
+ if institution and vars:
286
+ return institution, vars
287
+ except:
288
+ pass
289
+ try:
290
+ with open(filename) as conf_file:
291
+ config = json.load(conf_file)
292
+
293
+ institution = config.pop("institution", "KTH Royal Institute of Technology")
294
+ return institution, config
295
+ except:
296
+ pass
297
+ try:
298
+ institution = keyring.get_password("ladok3", "institution")
299
+ vars_keys = keyring.get_password("ladok3", "vars")
300
+
301
+ vars = {}
302
+ for key in vars_keys.split(";"):
303
+ value = keyring.get_password("ladok3", key)
304
+ if value:
305
+ vars[key] = value
306
+
307
+ if institution and vars:
308
+ return institution, vars
309
+ except:
310
+ pass
311
+ try:
312
+ institution = "KTH Royal Institute of Technology"
313
+ username = keyring.get_password("ladok3", "username")
314
+ password = keyring.get_password("ladok3", "password")
315
+ if username and password:
316
+ return institution, {"username": username, "password": password}
317
+ except:
318
+ pass
319
+
320
+ return None, None
321
+
322
+
268
323
  def clear_cache(ls, args):
269
- try:
270
- os.remove(dirs.user_cache_dir + "/LadokSession")
271
- except FileNotFoundError as err:
272
- pass
324
+ """Clear the cached LADOK session data.
325
+
326
+ Removes the stored encrypted session file from the user's cache directory.
327
+ Silently ignores if the file doesn't exist.
328
+
329
+ Args:
330
+ ls (LadokSession): The LADOK session (unused but required by interface).
331
+ args: Command line arguments (unused).
332
+ """
333
+ try:
334
+ os.remove(dirs.user_cache_dir + "/LadokSession")
335
+ except FileNotFoundError as err:
336
+ pass
337
+
338
+ sys.exit(0)
339
+
273
340
 
274
- sys.exit(0)
275
341
  def main():
276
- """Run the command-line interface for the ladok command"""
277
- argp = argparse.ArgumentParser(
278
- description="This is a CLI-ification of LADOK3's web GUI.",
279
- epilog="Web: https://github.com/dbosk/ladok3"
280
- )
281
- argp.add_argument("-f", "--config-file",
282
- default=f"{dirs.user_config_dir}/config.json",
283
- help="Path to configuration file "
284
- f"(default: {dirs.user_config_dir}/config.json) "
285
- "or set LADOK_USER and LADOK_PASS environment variables.")
286
- subp = argp.add_subparsers(
287
- title="commands",
288
- dest="command",
289
- required=True
290
- )
291
- login_parser = subp.add_parser("login",
292
- help="Manage login credentials",
293
- formatter_class=argparse.RawDescriptionHelpFormatter,
294
- description=f"""
342
+ """Run the command-line interface for the ladok command"""
343
+ argp = argparse.ArgumentParser(
344
+ description="This is a CLI-ification of LADOK3's web GUI.",
345
+ epilog="Web: https://github.com/dbosk/ladok3",
346
+ )
347
+ argp.add_argument(
348
+ "-f",
349
+ "--config-file",
350
+ default=f"{dirs.user_config_dir}/config.json",
351
+ help="Path to configuration file "
352
+ f"(default: {dirs.user_config_dir}/config.json) "
353
+ "or set LADOK_USER and LADOK_PASS environment variables.",
354
+ )
355
+ subp = argp.add_subparsers(title="commands", dest="command", required=True)
356
+ login_parser = subp.add_parser(
357
+ "login",
358
+ help="Manage login credentials",
359
+ formatter_class=argparse.RawDescriptionHelpFormatter,
360
+ description=f"""
295
361
  Manages the user's LADOK login credentials. There are three ways to supply the
296
362
  login credentials, in order of priority:
297
363
 
298
- 1) Through the system keyring: Just run `ladok login` and you'll be asked to
299
- enter the credentials and they will be stored in the keyring. Note that for
300
- this to work on the WSL platform (and possibly on Windows), you need to
301
- install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
364
+ 1) Through the environment: Just set the environment variables
302
365
 
303
- 2) Through the environment: Just set the environment variables
366
+ a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
367
+ Technology;
304
368
 
305
- a) LADOK_INST, the name of the institution, e.g. KTH Royal Institute of
306
- Technology;
307
- b) LADOK_VARS, a colon-separated list of environment variables, similarly to
308
- what's done in `ladok login` --- most don't need this, but can rather set
309
- LADOK_USER (the username, e.g. dbosk@ug.kth.se) and
310
- LADOK_PASS (the password) instead.
369
+ b) LADOK_VARS, a colon-separated list of environment variables, similarly to
370
+ what's done in `ladok login` --- most don't need this, but can rather set
371
+ LADOK_USER (the username, e.g. dbosk@ug.kth.se) and LADOK_PASS (the
372
+ password) instead.
311
373
 
312
- 3) Through the configuration file: Just write
374
+ 2) Through the configuration file: Just write
313
375
 
314
376
  {{
315
377
  "institution": "the name of the university"
@@ -321,42 +383,51 @@ def main():
321
383
  option). (The keys 'username' and 'password' can be renamed to correspond to
322
384
  the necessary values if the university login system uses other names.)
323
385
 
324
- """)
325
-
326
- login_parser.set_defaults(func=update_credentials_in_keyring)
327
- cache_parser = subp.add_parser("cache",
328
- help="Manage cache",
329
- description="Manages the cache of LADOK data"
330
- )
331
- cache_subp = cache_parser.add_subparsers(
332
- title="subcommands",
333
- dest="subcommand",
334
- required=True
335
- )
336
- cache_clear = cache_subp.add_parser("clear",
337
- help="Clear the cache",
338
- description="Clears everything from the cache"
339
- )
340
- cache_clear.set_defaults(func=clear_cache)
341
- ladok3.data.add_command_options(subp)
342
- ladok3.report.add_command_options(subp)
343
- ladok3.student.add_command_options(subp)
344
- argcomplete.autocomplete(argp)
345
- args = argp.parse_args()
346
- LADOK_INST, LADOK_VARS = load_credentials(args.config_file)
347
- try:
348
- ls = restore_ladok_session(LADOK_VARS)
349
- except ValueError as error:
350
- err(-1, f"Couldn't restore LADOK session: {error}")
351
- if not ls:
352
- ls = ladok3.LadokSession(LADOK_INST, vars=LADOK_VARS)
353
- if "func" in args:
354
- args.func(ls, args)
355
- store_ladok_session(ls, LADOK_VARS)
386
+ 3) Through the system keyring: Just run `ladok login` and you'll be asked to
387
+ enter the credentials and they will be stored in the keyring. Note that for
388
+ this to work on the WSL platform (and possibly on Windows), you need to
389
+ install the `keyrings.alt` package: `python3 -m pip install keyrings.alt`.
390
+
391
+ The keyring is the most secure. However, sometimes one want to try different
392
+ credentials, so the environment should override the keyring. Also, on WSL the
393
+ keyring might require you to enter a password in the terminal---this is very
394
+ inconvenient in scripts. However, when logging in, we first try to store the
395
+ credentials in the keyring.
396
+
397
+ """,
398
+ )
399
+
400
+ login_parser.set_defaults(func=update_credentials_in_keyring)
401
+ cache_parser = subp.add_parser(
402
+ "cache", help="Manage cache", description="Manages the cache of LADOK data"
403
+ )
404
+ cache_subp = cache_parser.add_subparsers(
405
+ title="subcommands", dest="subcommand", required=True
406
+ )
407
+ cache_clear = cache_subp.add_parser(
408
+ "clear", help="Clear the cache", description="Clears everything from the cache"
409
+ )
410
+ cache_clear.set_defaults(func=clear_cache)
411
+ ladok3.data.add_command_options(subp)
412
+ ladok3.report.add_command_options(subp)
413
+ ladok3.student.add_command_options(subp)
414
+ argcomplete.autocomplete(argp)
415
+ args = argp.parse_args()
416
+ LADOK_INST, LADOK_VARS = load_credentials(args.config_file)
417
+ try:
418
+ ls = restore_ladok_session(LADOK_VARS)
419
+ except ValueError as error:
420
+ err(-1, f"Couldn't restore LADOK session: {error}")
421
+ if not ls:
422
+ ls = ladok3.LadokSession(LADOK_INST, vars=LADOK_VARS)
423
+ if "func" in args:
424
+ args.func(ls, args)
425
+ store_ladok_session(ls, LADOK_VARS)
426
+
356
427
 
357
428
  if __name__ == "__main__":
358
- try:
359
- main()
360
- sys.exit(0)
361
- except Exception as e:
362
- err(-1, e)
429
+ try:
430
+ main()
431
+ sys.exit(0)
432
+ except Exception as e:
433
+ err(-1, e)