making-with-code-cli 5.1.0__tar.gz → 5.2.1__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.
Files changed (45) hide show
  1. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/PKG-INFO +1 -1
  2. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/pyproject.toml +3 -1
  3. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/curriculum.py +9 -3
  4. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/git_backend/mwc_backend.py +31 -7
  5. making_with_code_cli-5.2.1/src/making_with_code_cli/helpers.py +38 -0
  6. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/mwc_accounts_api.py +12 -1
  7. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/settings.py +1 -1
  8. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/setup/__init__.py +7 -9
  9. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/setup/tasks.py +61 -62
  10. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/section/__init__.py +9 -2
  11. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/section/edit.py +8 -2
  12. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/section/show.py +7 -2
  13. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/setup.py +2 -8
  14. making_with_code_cli-5.2.1/src/making_with_code_cli/teach/student/__init__.py +12 -0
  15. making_with_code_cli-5.1.0/src/making_with_code_cli/teach/student/update.py → making_with_code_cli-5.2.1/src/making_with_code_cli/teach/student/edit.py +9 -4
  16. making_with_code_cli-5.2.1/src/making_with_code_cli/teach/student/remove.py +34 -0
  17. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/update/__init__.py +1 -1
  18. making_with_code_cli-5.1.0/src/making_with_code_cli/helpers.py +0 -22
  19. making_with_code_cli-5.1.0/src/making_with_code_cli/teach/student/__init__.py +0 -10
  20. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/README.md +0 -0
  21. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/__init__.py +0 -0
  22. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/cli.py +0 -0
  23. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/decorators.py +0 -0
  24. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/errors.py +0 -0
  25. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/git_backend/__init__.py +0 -0
  26. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/git_backend/base_backend.py +0 -0
  27. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/git_wrapper.py +0 -0
  28. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/styles.py +0 -0
  29. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/submit.py +0 -0
  30. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/__init__.py +0 -0
  31. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/assess.py +0 -0
  32. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/check/__init__.py +0 -0
  33. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/check/check_module.py +0 -0
  34. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/gitea_api/api.py +0 -0
  35. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/gitea_api/exceptions.py +0 -0
  36. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/log.py +0 -0
  37. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/patch.py +0 -0
  38. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/section/create.py +0 -0
  39. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/section/delete.py +0 -0
  40. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/status.py +0 -0
  41. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/student/create.py +0 -0
  42. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/student_repo_functions.py +0 -0
  43. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/student_repos.py +0 -0
  44. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/teach/update.py +0 -0
  45. {making_with_code_cli-5.1.0 → making_with_code_cli-5.2.1}/src/making_with_code_cli/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: making-with-code-cli
3
- Version: 5.1.0
3
+ Version: 5.2.1
4
4
  Summary: Courseware for Making With Code
5
5
  Author: Chris Proctor
6
6
  Author-email: Chris Proctor <chris@chrisproctor.net>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "making-with-code-cli"
3
- version = "5.1.0"
3
+ version = "5.2.1"
4
4
  description = "Courseware for Making With Code"
5
5
  authors = [{ name = "Chris Proctor", email = "chris@chrisproctor.net" }]
6
6
  requires-python = ">=3.11,<4.0"
@@ -41,3 +41,5 @@ docs = [
41
41
  "sphinx>=8.2.3,<9",
42
42
  "sphinx-rtd-theme>=3.0.2,<4",
43
43
  ]
44
+ [tool.uv]
45
+ check-url = "https://pypi.org/simple/"
@@ -12,10 +12,16 @@ def get_curriculum(mwc_site_url, course_name=None):
12
12
  Returns the curriculum metadata for course_name.
13
13
  """
14
14
  url = mwc_site_url + "/manifest"
15
- response = requests.get(url)
15
+ try:
16
+ response = requests.get(url, timeout=15)
17
+ except requests.exceptions.RequestException as e:
18
+ raise CurriculumSiteNotAvailable(mwc_site_url) from e
16
19
  if response.ok:
17
- text = response.text.strip(LIVE_RELOAD)
18
- metadata = json.loads(text)
20
+ try:
21
+ text = response.text.strip(LIVE_RELOAD)
22
+ metadata = json.loads(text)
23
+ except (json.JSONDecodeError, ValueError) as e:
24
+ raise CurriculumSiteNotAvailable(mwc_site_url) from e
19
25
  if course_name:
20
26
  for course in metadata["courses"]:
21
27
  if course["name"] == course_name:
@@ -20,10 +20,17 @@ from making_with_code_cli.errors import (
20
20
  GitServerNotAvailable,
21
21
  )
22
22
 
23
+ EDITOR_COMMANDS = {
24
+ "atom": "atom --wait",
25
+ "code": "code --wait",
26
+ "subl": "subl -n -w",
27
+ "mate": "mate -w",
28
+ "vim": "vim",
29
+ "emacs": "emacs",
30
+ }
31
+
23
32
  class MWCBackend(GitBackend):
24
33
  """A Github backend. Students own their own repos and grant teachers access via token.
25
- Note that this gives the teacher account access to the student's entire github account,
26
- within scope.
27
34
  """
28
35
 
29
36
  MWC_GIT_PROTOCOL = "https"
@@ -31,7 +38,7 @@ class MWCBackend(GitBackend):
31
38
  COMMIT_TEMPLATE = ".commit_template"
32
39
 
33
40
  def init_module(self, module, modpath):
34
- """Creates the named repo from a template.
41
+ """Creates the named repo from a template.
35
42
  """
36
43
  self.check_settings()
37
44
  server, repo_owner, repo_name = self.parse_repo_url(module["repo_url"])
@@ -42,19 +49,36 @@ class MWCBackend(GitBackend):
42
49
  self.create_from_template(repo_owner, repo_name)
43
50
  with cd(modpath.parent):
44
51
  self.clone_repo(repo_name)
52
+ self.configure_git(modpath)
45
53
  if (modpath / self.COMMIT_TEMPLATE).exists():
46
- run(f"git config commit.template {self.COMMIT_TEMPLATE}", shell=True,
54
+ run(["git", "config", "--local", "commit.template", self.COMMIT_TEMPLATE],
47
55
  check=True, cwd=modpath)
48
56
  run("uv venv", shell=True, check=True, cwd=modpath, capture_output=True)
49
57
  self.init_direnv(modpath)
50
58
 
59
+ def configure_git(self, modpath):
60
+ """Configures git user settings locally for the repo."""
61
+ name = self.settings['mwc_username']
62
+ run(["git", "config", "--local", "user.name", name], check=True, cwd=modpath)
63
+ run(["git", "config", "--local", "user.email", "nobody@makingwithcode.org"],
64
+ check=True, cwd=modpath)
65
+ editor = self.settings.get('editor')
66
+ if editor in EDITOR_COMMANDS:
67
+ run(["git", "config", "--local", "core.editor", EDITOR_COMMANDS[editor]],
68
+ check=True, cwd=modpath)
69
+
51
70
  def update(self, module, modpath, install=True):
52
71
  if (modpath / ".git").is_dir():
53
72
  with cd(modpath):
54
73
  relpath = self.relative_path(modpath)
55
74
  click.echo(address(f"Checking {relpath} for updates.", preformatted=True))
56
- gitresult = run("git pull", shell=True, check=True, capture_output=True,
57
- text=True)
75
+ gitresult = run("git pull", shell=True, capture_output=True, text=True)
76
+ if gitresult.returncode != 0:
77
+ click.echo(error(
78
+ f"Could not update {relpath}: {gitresult.stderr.strip()}\n"
79
+ "You may have local changes that conflict. Ask a teacher for help."
80
+ ))
81
+ return
58
82
  click.echo(info(gitresult.stdout))
59
83
  if install and Path("pyproject.toml").exists():
60
84
  result = run("uv sync", shell=True, capture_output=True, text=True, cwd=modpath)
@@ -92,7 +116,7 @@ class MWCBackend(GitBackend):
92
116
  user = self.settings['mwc_username']
93
117
  auth = user + ':' + self.settings['mwc_git_token']
94
118
  url = f"{self.MWC_GIT_PROTOCOL}://{auth}@{self.MWC_GIT_SERVER}/{user}/{repo_name}.git"
95
- run(f"git clone {url}", shell=True, check=True, capture_output=True)
119
+ run(["git", "clone", url], check=True, capture_output=True)
96
120
 
97
121
  def check_settings(self):
98
122
  if "mwc_username" not in self.settings:
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+ from contextlib import contextmanager
3
+ import dateparser
4
+ import os
5
+ import csv
6
+ from making_with_code_cli.errors import MWCError
7
+
8
+ @contextmanager
9
+ def cd(path):
10
+ """Sets the cwd within the context
11
+ """
12
+ origin = Path().resolve()
13
+ try:
14
+ os.chdir(path)
15
+ yield
16
+ finally:
17
+ os.chdir(origin)
18
+
19
+ def in_bounds(value, minimum=None, maximum=None):
20
+ return (not minimum or value >= minimum) and (not maximum or value <= maximum)
21
+
22
+ def date_string(arg):
23
+ return dateparser.parse(arg, settings={'RETURN_AS_TIMEZONE_AWARE': True})
24
+
25
+ def read_roster_file(filepath):
26
+ try:
27
+ with open(filepath) as fh:
28
+ records = [record for record in csv.DictReader(fh)]
29
+ except Exception as err:
30
+ raise MWCError(f"Error reading roster file {filepath} as csv")
31
+ for i, record in enumerate(records):
32
+ if "username" not in record.keys():
33
+ raise MWCError(f"Error reading roster data from {filepath}: 'username' key missing in row {i}")
34
+ if "username" not in record.keys():
35
+ raise MWCError(f"Error reading roster data from {filepath}: 'password' key missing in row {i}")
36
+
37
+
38
+
@@ -1,3 +1,4 @@
1
+ import click
1
2
  import requests
2
3
  import json
3
4
  import os
@@ -5,12 +6,14 @@ from pathlib import Path
5
6
  from urllib.parse import urljoin
6
7
  from getpass import getpass
7
8
  from making_with_code_cli.errors import MWCError
9
+ from making_with_code_cli.styles import debug as debug_fmt
8
10
 
9
11
  MWC_ACCOUNTS_SERVER = "https://accounts.makingwithcode.org"
10
12
 
11
13
  class MWCAccountsAPI:
12
- def __init__(self, mwc_accounts_server=None):
14
+ def __init__(self, mwc_accounts_server=None, debug=False):
13
15
  self.mwc_accounts_server = mwc_accounts_server or MWC_ACCOUNTS_SERVER
16
+ self.debug = debug
14
17
 
15
18
  def login(self, username, password):
16
19
  "Authenticates with a username and password, returning an auth token"
@@ -34,6 +37,10 @@ class MWCAccountsAPI:
34
37
  response = self.post("/students", data=params, token=token)
35
38
  return self.handle_response(response)
36
39
 
40
+ def remove_student_from_section(self, token, params):
41
+ response = self.delete("/students", data=params, token=token)
42
+ return self.handle_response(response)
43
+
37
44
  def update_student(self, token, params):
38
45
  response = self.put("/students", data=params, token=token)
39
46
  return self.handle_response(response)
@@ -65,6 +72,10 @@ class MWCAccountsAPI:
65
72
  def http_request(self, method, url, data=None, token=None):
66
73
  fn = getattr(requests, method)
67
74
  headers = {"Authorization": f"Token {token}"} if token else None
75
+ if self.debug:
76
+ click.echo(debug_fmt(
77
+ f"{method} {self.mwc_accounts_server + url}, data={data}"
78
+ ))
68
79
  try:
69
80
  return fn(self.mwc_accounts_server + url, data=data, headers=headers)
70
81
  except requests.exceptions.ConnectionError:
@@ -35,7 +35,7 @@ def iter_settings(settings, prefix=None):
35
35
  for key, value in settings.items():
36
36
  keypath = (prefix or []) + [key]
37
37
  if isinstance(value, dict):
38
- for k, v in _iter_settings(value, prefix=keypath):
38
+ for k, v in iter_settings(value, prefix=keypath):
39
39
  yield '.'.join(keypath), v
40
40
  else:
41
41
  yield '.'.join(keypath), value
@@ -40,18 +40,18 @@ from making_with_code_cli.setup.tasks import (
40
40
  InstallImageMagick,
41
41
  InstallHttpie,
42
42
  InstallScipy,
43
- GitConfiguration,
43
+ InstallOllama,
44
+ InstallPandoc,
45
+ InstallPoppler,
44
46
  )
45
47
 
46
48
  @click.command()
47
49
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
48
50
  @click.option("--debug", is_flag=True, help="Show debug-level output")
49
- @click.option("--git-name", help="Set git name")
50
- @click.option("--git-email", help="Set git email address")
51
51
  @click.option("--mwc-accounts-url", help="Set URL for MWC accounts server")
52
52
  @click.pass_context
53
53
  @handle_mwc_errors
54
- def setup(ctx, config, debug, git_name, git_email, mwc_accounts_url):
54
+ def setup(ctx, config, debug, mwc_accounts_url):
55
55
  """Set up the MWC command line interface"""
56
56
  settings = read_settings(config)
57
57
  sp = get_settings_path(config)
@@ -68,10 +68,6 @@ def setup(ctx, config, debug, git_name, git_email, mwc_accounts_url):
68
68
  for note in INTRO_NOTES:
69
69
  click.echo(address(note, list_format=True))
70
70
  click.echo()
71
- if git_name:
72
- settings['git_name'] = git_name
73
- if git_email:
74
- settings['git_email'] = git_email
75
71
  if mwc_accounts_url:
76
72
  settings['mwc_accounts_url'] = mwc_accounts_url
77
73
  settings['mwc_username'] = choose_mwc_username(settings.get("mwc_username"))
@@ -112,7 +108,9 @@ def setup(ctx, config, debug, git_name, git_email, mwc_accounts_url):
112
108
  InstallVSCode,
113
109
  InstallImageMagick,
114
110
  InstallHttpie,
115
- GitConfiguration,
111
+ InstallOllama,
112
+ InstallPandoc,
113
+ InstallPoppler,
116
114
  ]
117
115
  errors = []
118
116
  for task_class in task_classes:
@@ -28,6 +28,7 @@ from making_with_code_cli.errors import (
28
28
  )
29
29
  import click
30
30
  import requests
31
+ import shutil
31
32
  from subprocess import run
32
33
  import yaml
33
34
 
@@ -54,7 +55,8 @@ class PlatformNotSupported(Exception):
54
55
  class Platform(Flag):
55
56
  MAC = auto()
56
57
  UBUNTU = auto()
57
- SUPPORTED = MAC | UBUNTU
58
+ WSL = auto()
59
+ SUPPORTED = MAC | UBUNTU | WSL
58
60
  UNSUPPORTED = 0
59
61
 
60
62
  @classmethod
@@ -63,6 +65,11 @@ class Platform(Flag):
63
65
  if system_name == "Darwin":
64
66
  return cls.MAC
65
67
  if system_name == "Linux":
68
+ try:
69
+ if "microsoft" in Path("/proc/version").read_text().lower():
70
+ return cls.WSL
71
+ except OSError:
72
+ pass
66
73
  return cls.UBUNTU
67
74
  return cls.UNSUPPORTED
68
75
 
@@ -70,16 +77,16 @@ class Platform(Flag):
70
77
  def package_manager(cls, brew_cask=False):
71
78
  """Returns the command for the platform's package manager.
72
79
  """
73
- platform = cls.detect()
74
- if platform == cls.MAC:
75
- if brew_cask:
80
+ p = cls.detect()
81
+ if p == cls.MAC:
82
+ if brew_cask:
76
83
  return "brew install --cask "
77
84
  else:
78
85
  return "brew install "
79
- elif platform == cls.UBUNTU:
86
+ elif p in (cls.UBUNTU, cls.WSL):
80
87
  return "sudo apt install "
81
88
  else:
82
- raise cls.NotSupported()
89
+ raise PlatformNotSupported()
83
90
 
84
91
  def default_work_dir(teacher=False):
85
92
  dirname = "making_with_code_teacher" if teacher else "making_with_code"
@@ -120,7 +127,7 @@ def choose_work_dir(default=None, teacher=False):
120
127
  default=default or default_work_dir(teacher=teacher),
121
128
  type=click.Path(path_type=Path),
122
129
  )
123
- work_dir = work_dir.expanduser()
130
+ work_dir = work_dir.expanduser().resolve()
124
131
  if work_dir.is_file():
125
132
  click.echo(error("There's already a file at that location."))
126
133
  elif work_dir.exists():
@@ -168,15 +175,15 @@ def choose_editor(default=None):
168
175
  click.echo(error(f"Couldn't find {ed}. Double-check that it's installed."))
169
176
 
170
177
  def editor_installed(ed):
171
- return bool(run(f"which {ed}", shell=True, capture_output=True).stdout)
178
+ return shutil.which(ed) is not None
172
179
 
173
180
  def get_shell_name():
174
181
  shellpath = run("echo $SHELL", shell=True, capture_output=True, text=True)
175
182
  return shellpath.stdout.split('/')[-1].strip()
176
183
 
177
184
  def get_mwc_rc_path():
178
- xdg_config_home = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
179
- return xdg_config_home / "mwc" / "shell_config.sh"
185
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
186
+ return Path(xdg_config_home) / "mwc" / "shell_config.sh"
180
187
 
181
188
  def platform_rc_file(debug=False):
182
189
  shell = get_shell_name()
@@ -234,7 +241,7 @@ class SetupTask:
234
241
  click.echo(debug_fmt(message))
235
242
 
236
243
  def executable_on_path(self, name):
237
- return bool(run(f"which {name}", shell=True, capture_output=True).stdout)
244
+ return shutil.which(name) is not None
238
245
 
239
246
  class WriteMWCShellConfig(SetupTask):
240
247
  description = "Write the MWC shell config file to ~/.config/mwc/shell_config.sh or XDG_CONFIG_HOME"
@@ -319,7 +326,7 @@ class SuppressDirenvDiffs(SetupTask):
319
326
  tomlkit.dump(direnv_conf, fh)
320
327
 
321
328
  def get_direnv_config_file(self):
322
- config_home = os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")
329
+ config_home = os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))
323
330
  return Path(config_home) / "direnv" / "direnv.toml"
324
331
 
325
332
 
@@ -328,12 +335,13 @@ class InstallXCode(SetupTask):
328
335
  platform = Platform.MAC
329
336
 
330
337
  def is_complete(self):
331
- return bool(run("xcode-select -p", shell=True, capture_output=True, check=True).stdout)
338
+ result = run("xcode-select -p", shell=True, capture_output=True)
339
+ return result.returncode == 0 and bool(result.stdout)
332
340
 
333
341
  def run_task(self):
334
342
  msg = (
335
- "Installing Xcode... (this may take a while)"
336
- "Please click \"Install\" and accept the license agreement. "
343
+ "Installing Xcode... (this may take a while)\n"
344
+ "Please click \"Install\" and accept the license agreement."
337
345
  )
338
346
  click.echo(address(msg))
339
347
  run("xcode-select --install", shell=True, check=True)
@@ -358,7 +366,7 @@ class InstallPackage(SetupTask):
358
366
  system_platform = Platform.detect()
359
367
  if system_platform == Platform.MAC:
360
368
  return self.brew_name
361
- elif system_platform == Platform.UBUNTU:
369
+ elif system_platform in (Platform.UBUNTU, Platform.WSL):
362
370
  return self.apt_name
363
371
  else:
364
372
  raise PlatformNotSupported()
@@ -390,17 +398,34 @@ class InstallTree(InstallPackage):
390
398
  executable_name = brew_name = apt_name = nix_name = "tree"
391
399
 
392
400
  class InstallVSCode(InstallPackage):
393
- platform = Platform.MAC
401
+ platform = Platform.MAC | Platform.UBUNTU | Platform.WSL
394
402
  executable_name = "code"
395
403
  brew_name = "visual-studio-code"
396
- cask = True
404
+ brew_cask = True
405
+
406
+ def is_complete(self):
407
+ if Platform.detect() == Platform.MAC:
408
+ app_locations = [
409
+ Path("/Applications/Visual Studio Code.app"),
410
+ Path.home() / "Applications/Visual Studio Code.app",
411
+ ]
412
+ return self.executable_on_path("code") or any(p.exists() for p in app_locations)
413
+ return self.executable_on_path("code")
397
414
 
398
415
  def run_task(self):
399
- platform = Platform.detect()
400
- if platform & Platform.UBUNTU:
416
+ if Platform.detect() in (Platform.UBUNTU, Platform.WSL):
401
417
  run("sudo snap install --classic code", shell=True, check=True)
402
418
  else:
403
- return super().run_task()
419
+ # If brew has a stale record of VS Code but the app is missing,
420
+ # the upgrade will fail. Zap the cask first to clear brew's state.
421
+ stale = run(
422
+ "brew list --cask visual-studio-code",
423
+ shell=True, capture_output=True
424
+ ).returncode == 0
425
+ if stale:
426
+ run("brew uninstall --cask --zap --force visual-studio-code",
427
+ shell=True, check=True)
428
+ super().run_task()
404
429
 
405
430
  class InstallImageMagick(InstallPackage):
406
431
  executable_name = "magick"
@@ -419,50 +444,24 @@ class InstallScipy(InstallPackage):
419
444
  def is_complete(self):
420
445
  return find_spec("scipy") is not None
421
446
 
422
- class GitConfiguration(SetupTask):
423
- """Configure global git settings.
424
- Can be skipped by setting `skip_git_config: true` in settings.
425
- """
426
- description = "Configure git"
427
-
428
- editorcmds = {
429
- "atom": '"atom --wait"',
430
- "code": '"code --wait"',
431
- "subl": '"subl -n -w"',
432
- "mate": '"mate -w"',
433
- "vim": "vim",
434
- "emacs": "emacs",
435
- }
436
-
437
- def is_complete(self):
438
- if self.settings.get("skip_git_config"):
439
- confirm("Skipping git configuration because 'skip_git_config' is set in MWC settings")
440
- return True
441
- for key, value in self.get_expected_git_config().items():
442
- expected = value.strip().strip('"')
443
- observed = self.read_git_config(key).strip().strip('"')
444
- if expected != observed:
445
- return False
446
- return True
447
+ class InstallOllama(InstallPackage):
448
+ executable_name = "ollama"
449
+ brew_name = "ollama"
447
450
 
448
451
  def run_task(self):
449
- (Path.home() / ".gitconfig").touch()
450
- for key, val in self.get_expected_git_config().items():
451
- run(f'git config --global --replace-all {key} {val}', shell=True, check=True)
452
+ if Platform.detect() in (Platform.UBUNTU, Platform.WSL):
453
+ click.echo(address("Installing ollama..."))
454
+ run("curl -fsSL https://ollama.com/install.sh | sh", shell=True, check=True)
455
+ else:
456
+ super().run_task()
452
457
 
453
- def read_git_config(self, setting):
454
- "Reads current git config setting"
455
- result = run(f"git config --get {setting}", shell=True, capture_output=True, text=True)
456
- return result.stdout.strip()
458
+ class InstallPandoc(InstallPackage):
459
+ executable_name = brew_name = apt_name = "pandoc"
460
+
461
+ class InstallPoppler(InstallPackage):
462
+ executable_name = "pdftotext"
463
+ brew_name = "poppler"
464
+ apt_name = "poppler-utils"
457
465
 
458
- def get_expected_git_config(self):
459
- """Returns a dict containing expected git configuration settings.
460
- """
461
- git_config = {"init.defaultBranch": "main"}
462
- if self.settings.get('editor') in self.editorcmds:
463
- git_config["core.editor"] = self.editorcmds[self.settings.get('editor')]
464
- git_config["user.name"] = self.settings.get('git_name', self.settings['mwc_username'])
465
- git_config["user.email"] = self.settings.get('git_email', 'nobody@makingwithcode.org')
466
- return git_config
467
466
 
468
467
 
@@ -1,25 +1,32 @@
1
1
  import click
2
2
  from csv import DictWriter
3
3
  from tabulate import tabulate
4
- from making_with_code_cli.settings import read_settings
4
+ from making_with_code_cli.settings import read_settings, get_settings_path
5
5
  from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
6
6
  from making_with_code_cli.styles import (
7
7
  info,
8
+ debug as debug_fmt,
8
9
  )
9
10
  from making_with_code_cli.teach.setup import check_required_teacher_settings
10
11
  from making_with_code_cli.teach.section.create import create_section
11
12
  from making_with_code_cli.teach.section.show import show_section
12
13
  from making_with_code_cli.teach.section.edit import edit_section
13
14
  from making_with_code_cli.teach.section.delete import delete_section
15
+ from making_with_code_cli.decorators import handle_mwc_errors
14
16
 
15
17
  @click.group(invoke_without_command=True)
16
18
  @click.pass_context
17
19
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
18
20
  @click.option("-o", "--outfile", help="Save results as csv")
19
- def section(ctx, config, outfile):
21
+ @click.option("--debug", is_flag=True, help="Show debug-level output")
22
+ @handle_mwc_errors
23
+ def section(ctx, config, outfile, debug):
20
24
  "Manage sections of students"
21
25
  if ctx.invoked_subcommand is None:
22
26
  settings = read_settings(config)
27
+ if debug:
28
+ sp = get_settings_path(config)
29
+ click.echo(debug_fmt(f"Reading settings from {sp}"))
23
30
  if not check_required_teacher_settings(settings):
24
31
  return
25
32
  api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
@@ -4,6 +4,7 @@ from making_with_code_cli.settings import read_settings
4
4
  from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
5
5
  from making_with_code_cli.curriculum import get_curriculum
6
6
  from making_with_code_cli.errors import CurriculumSiteNotAvailable, MWCError
7
+ from making_with_code_cli.decorators import handle_mwc_errors
7
8
  from making_with_code_cli.styles import (
8
9
  address,
9
10
  error,
@@ -18,10 +19,15 @@ from making_with_code_cli.styles import (
18
19
  @click.option("--curriculum-site-url", help="URL for curriculum website. e.g. https://makingwithcode.org")
19
20
  @click.option("--course-name", help="MWC course name")
20
21
  @click.option("--code", help="Code students can use to join the section")
21
- @click.option("--roster", help="csv file containing student information")
22
- def edit_section(slug, config, name, curriculum_site_url, course_name, code, roster):
22
+ @click.option("--roster", type=click.Path(exists=True), help="csv file containing student information")
23
+ @click.option('-r', "--remove", is_flag=True, help="When using a roster, remove existing students not on roster")
24
+ @click.option('-f', "--force", is_flag=True, help="When using a roster, update existing student passwords")
25
+ @handle_mwc_errors
26
+ def edit_section(slug, config, name, curriculum_site_url, course_name, code, roster, remove, force):
23
27
  "Edit an existing section"
24
28
  settings = read_settings(config)
29
+ if roster:
30
+ roster_data = read_roster_file(roster)
25
31
  api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
26
32
  params = {"slug": slug}
27
33
  if name:
@@ -1,9 +1,10 @@
1
1
  import click
2
2
  from csv import DictWriter
3
- from making_with_code_cli.settings import read_settings
3
+ from making_with_code_cli.settings import read_settings, get_settings_path
4
4
  from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
5
5
  from making_with_code_cli.styles import (
6
6
  address,
7
+ debug as debug_fmt,
7
8
  error,
8
9
  info,
9
10
  question,
@@ -13,9 +14,13 @@ from making_with_code_cli.styles import (
13
14
  @click.argument("slug")
14
15
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
15
16
  @click.option("-o", "--outfile", help="Save results as csv")
16
- def show_section(config, slug, outfile):
17
+ @click.option("--debug", is_flag=True, help="Show debug-level output")
18
+ def show_section(config, slug, outfile, debug):
17
19
  "Show section details"
18
20
  settings = read_settings(config)
21
+ if debug:
22
+ sp = get_settings_path(config)
23
+ click.echo(debug_fmt(f"Reading settings from {sp}"))
19
24
  api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
20
25
  params = {"slug": slug}
21
26
  response = api.get_roster(settings.get('mwc_accounts_token'))
@@ -46,10 +46,8 @@ def check_required_teacher_settings(settings):
46
46
  @click.command
47
47
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
48
48
  @click.option("--debug", is_flag=True, help="Show debug-level output")
49
- @click.option("--git-name", help="Set git name")
50
- @click.option("--git-email", help="Set git email address")
51
49
  @click.option("--mwc-accounts-url", help="Set URL for MWC accounts server")
52
- def setup(config, debug, git_name, git_email, mwc_accounts_url):
50
+ def setup(config, debug, mwc_accounts_url):
53
51
  """Configure teacher settings"""
54
52
  settings = read_settings(config)
55
53
  if debug:
@@ -57,10 +55,6 @@ def setup(config, debug, git_name, git_email, mwc_accounts_url):
57
55
  click.echo(debug_fmt(f"Reading settings from {sp}"))
58
56
  click.echo(address(INTRO_MESSAGE))
59
57
  click.echo()
60
- if git_name:
61
- settings['git_name'] = git_name
62
- if git_email:
63
- settings['git_email'] = git_email
64
58
  if mwc_accounts_url:
65
59
  settings['mwc_accounts_url'] = mwc_accounts_url
66
60
  settings['mwc_username'] = choose_mwc_username(settings.get("mwc_username"))
@@ -78,7 +72,7 @@ def setup(config, debug, git_name, git_email, mwc_accounts_url):
78
72
  token = prompt_mwc_password(settings['mwc_username'], api)
79
73
  settings['mwc_accounts_token'] = token
80
74
  settings['mwc_git_token'] = status['git_token']
81
- settings['work_dir'] = str(choose_work_dir(settings.get("work_dir")).resolve())
75
+ settings['work_dir'] = str(choose_work_dir(settings.get("work_dir")))
82
76
  settings['editor'] = choose_editor(settings.get('editor', 'code'))
83
77
  settings['teacher_work_dir'] = str(choose_work_dir(
84
78
  settings.get("teacher_work_dir"),
@@ -0,0 +1,12 @@
1
+ import click
2
+ from making_with_code_cli.teach.student.create import create_student
3
+ from making_with_code_cli.teach.student.edit import edit_student
4
+ from making_with_code_cli.teach.student.remove import remove_student_from_section
5
+
6
+ @click.group()
7
+ def student():
8
+ "Manage students"
9
+
10
+ student.add_command(create_student)
11
+ student.add_command(edit_student)
12
+ student.add_command(remove_student_from_section)
@@ -1,19 +1,24 @@
1
1
  import click
2
2
  from tabulate import tabulate
3
- from making_with_code_cli.settings import read_settings
3
+ from making_with_code_cli.settings import read_settings, get_settings_path
4
4
  from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
5
5
  from making_with_code_cli.styles import (
6
6
  info,
7
+ debug as debug_fmt,
7
8
  error,
8
9
  )
9
10
 
10
- @click.command("update")
11
+ @click.command("edit")
11
12
  @click.argument("username")
12
13
  @click.argument("password")
13
14
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
14
- def update_student(username, password, config):
15
- "Update a student's password"
15
+ @click.option("--debug", is_flag=True, help="Show debug-level output")
16
+ def edit_student(username, password, config, debug):
17
+ "Edit a student's password"
16
18
  settings = read_settings(config)
19
+ if debug:
20
+ sp = get_settings_path(config)
21
+ click.echo(debug_fmt(f"Reading settings from {sp}"))
17
22
  api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
18
23
  params = {
19
24
  "username": username,
@@ -0,0 +1,34 @@
1
+ import click
2
+ from making_with_code_cli.settings import read_settings, get_settings_path
3
+ from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
4
+ from making_with_code_cli.styles import (
5
+ info,
6
+ debug as debug_fmt,
7
+ error,
8
+ )
9
+
10
+ @click.command("remove")
11
+ @click.argument("username")
12
+ @click.argument("section")
13
+ @click.option("--config", help="Path to config file (default: ~/.mwc)")
14
+ @click.option("--debug", is_flag=True, help="Show debug-level output")
15
+ def remove_student_from_section(username, section, config, debug):
16
+ "Remove a student from a section"
17
+ settings = read_settings(config)
18
+ if debug:
19
+ sp = get_settings_path(config)
20
+ click.echo(debug_fmt(f"Reading settings from {sp}"))
21
+ api = MWCAccountsAPI(settings.get('mwc_accounts_url'), debug=debug)
22
+ params = {
23
+ "username": username,
24
+ "section": section,
25
+ }
26
+ try:
27
+ response = api.remove_student_from_section(settings.get('mwc_accounts_token'), params)
28
+ click.echo(info(f"Removed student {username} from {section}."))
29
+ except api.RequestFailed as err:
30
+ click.echo(error(f"Could not remove {params['username']} from {params['section']}:"))
31
+ for field, problems in err.data.items():
32
+ click.echo(error(f" - {field}: {'; '.join(problems)}"))
33
+
34
+
@@ -22,8 +22,8 @@ from making_with_code_cli.styles import (
22
22
  def update(config, debug):
23
23
  """Update the MWC work directory"""
24
24
  settings = read_settings(config)
25
- sp = get_settings_path(config)
26
25
  if debug:
26
+ sp = get_settings_path(config)
27
27
  click.echo(debug_fmt(f"Reading settings from {sp}"))
28
28
  if not settings:
29
29
  click.echo(error(f"Please run mwc setup first."))
@@ -1,22 +0,0 @@
1
- from pathlib import Path
2
- from contextlib import contextmanager
3
- import dateparser
4
- import os
5
-
6
- @contextmanager
7
- def cd(path):
8
- """Sets the cwd within the context
9
- """
10
- origin = Path().resolve()
11
- try:
12
- os.chdir(path)
13
- yield
14
- finally:
15
- os.chdir(origin)
16
-
17
- def in_bounds(value, minimum=None, maximum=None):
18
- return (not minimum or value >= minimum) and (not maximum or value <= maximum)
19
-
20
- def date_string(arg):
21
- return dateparser.parse(arg, settings={'RETURN_AS_TIMEZONE_AWARE': True})
22
-
@@ -1,10 +0,0 @@
1
- import click
2
- from making_with_code_cli.teach.student.create import create_student
3
- from making_with_code_cli.teach.student.update import update_student
4
-
5
- @click.group()
6
- def student():
7
- "Manage students"
8
-
9
- student.add_command(create_student)
10
- student.add_command(update_student)