making-with-code-cli 4.0.0__tar.gz → 4.1.0__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 (40) hide show
  1. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/PKG-INFO +1 -1
  2. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/helpers.py +7 -0
  3. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/setup/tasks.py +2 -4
  4. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/log.py +21 -28
  5. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/section/__init__.py +2 -0
  6. making_with_code_cli-4.1.0/making_with_code_cli/teach/section/show.py +53 -0
  7. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/status.py +15 -3
  8. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/student_repo_functions.py +23 -14
  9. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/student_repos.py +2 -2
  10. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/pyproject.toml +1 -1
  11. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/README.md +0 -0
  12. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/cli.py +0 -0
  13. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/curriculum.py +0 -0
  14. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/decorators.py +0 -0
  15. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/errors.py +0 -0
  16. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/git_backend/__init__.py +0 -0
  17. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/git_backend/base_backend.py +0 -0
  18. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/git_backend/mwc_backend.py +0 -0
  19. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/git_wrapper.py +0 -0
  20. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/mwc_accounts_api.py +0 -0
  21. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/settings.py +0 -0
  22. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/setup/__init__.py +0 -0
  23. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/styles.py +0 -0
  24. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/submit.py +0 -0
  25. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/__init__.py +0 -0
  26. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/assess.py +0 -0
  27. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/check/__init__.py +0 -0
  28. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/check/check_module.py +0 -0
  29. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/gitea_api/api.py +0 -0
  30. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/gitea_api/exceptions.py +0 -0
  31. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/patch.py +0 -0
  32. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/section/create.py +0 -0
  33. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/section/edit.py +0 -0
  34. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/setup.py +0 -0
  35. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/student/__init__.py +0 -0
  36. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/student/create.py +0 -0
  37. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/student/update.py +0 -0
  38. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/teach/update.py +0 -0
  39. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/making_with_code_cli/update/__init__.py +0 -0
  40. {making_with_code_cli-4.0.0 → making_with_code_cli-4.1.0}/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: 4.0.0
3
+ Version: 4.1.0
4
4
  Summary: Courseware for Making With Code
5
5
  License: MIT
6
6
  Author: Chris Proctor
@@ -1,5 +1,6 @@
1
1
  from pathlib import Path
2
2
  from contextlib import contextmanager
3
+ import dateparser
3
4
  import os
4
5
 
5
6
  @contextmanager
@@ -13,3 +14,9 @@ def cd(path):
13
14
  finally:
14
15
  os.chdir(origin)
15
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
+
@@ -461,10 +461,8 @@ class GitConfiguration(SetupTask):
461
461
  git_config = {"init.defaultBranch": "main"}
462
462
  if self.settings.get('editor') in self.editorcmds:
463
463
  git_config["core.editor"] = self.editorcmds[self.settings.get('editor')]
464
- if self.settings.get('git_name'):
465
- git_config["user.name"] = self.settings['git_name']
466
- if self.settings.get('git_email'):
467
- git_config["user.email"] = self.settings['git_email']
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')
468
466
  return git_config
469
467
 
470
468
 
@@ -9,7 +9,7 @@ from git import Repo
9
9
  from pathlib import Path
10
10
  from threading import Thread, Semaphore
11
11
  from textwrap import wrap
12
- import dateparser
12
+ from making_with_code_cli.helpers import date_string, in_bounds
13
13
  from making_with_code_cli.teach.student_repos import StudentRepos
14
14
  from making_with_code_cli.settings import read_settings
15
15
  from making_with_code_cli.teach.setup import check_required_teacher_settings
@@ -22,31 +22,27 @@ from making_with_code_cli.styles import (
22
22
  error,
23
23
  )
24
24
 
25
- def date_string(arg):
26
- return dateparser.parse(arg, settings={'RETURN_AS_TIMEZONE_AWARE': True})
27
-
28
25
  @click.command()
29
26
  @click.option("--config", help="Path to config file (default: ~/.mwc)")
30
- @click.option('-g', "--group", help="Filter by group name")
27
+ @click.option('-s', "--section", help="Filter by section name/slug")
31
28
  @click.option('-c', "--course", help="Filter by course name")
32
29
  @click.option('-u', "--user", help="Filter by username")
33
30
  @click.option('-n', "--unit", help="Filter by unit name/slug")
34
31
  @click.option('-m', "--module", help="Filter by module name/slug")
35
- @click.option('-s', "--start", type=date_string, help="Start datetime")
36
- @click.option('-e', "--end", type=date_string, help="End datetime")
32
+ @click.option('-B', "--begin", type=date_string, help="Start datetime")
33
+ @click.option('-C', "--end", type=date_string, help="End datetime")
37
34
  @click.option('-U', "--update", is_flag=True, help="Update repos first")
38
35
  @click.option('-t', "--threads", type=int, default=8, help="Maximum simultaneous threads")
39
- def log(config, group, course, user, unit, module, start, end, update, threads):
36
+ def log(config, section, course, user, unit, module, begin, end, update, threads):
40
37
  "Show repo logs"
41
38
  if update:
42
39
  from making_with_code_cli.teach.update import update as update_task
43
- update_task.callback(config, group, course, user, unit, module, threads)
40
+ update_task.callback(config, section, course, user, unit, module, threads)
44
41
  settings = read_settings(config)
45
42
  if not check_required_teacher_settings(settings):
46
43
  return
47
44
  repos = StudentRepos(settings, threads)
48
- get_commits = get_repo_commits_factory(start, end)
49
- results = repos.apply(get_commits, group=group, course=course, user=user,
45
+ results = repos.apply(get_commits, section=section, course=course, user=user,
50
46
  unit=unit, module=module, status_message="Collecting logs")
51
47
  for repo in results:
52
48
  click.echo(address('-' * 80))
@@ -54,23 +50,20 @@ def log(config, group, course, user, unit, module, start, end, update, threads):
54
50
  for commit in repo['commits']:
55
51
  click.echo(info(format_commit(commit), preformatted=True))
56
52
 
57
- def get_repo_commits_factory(start, end):
58
- def get_repo_commits(semaphore, results, group, username, path, token):
59
- "Gets commits from repo"
60
- semaphore.acquire()
61
- if path.exists():
62
- repo = Repo(path)
63
- commits = []
64
- for commit in repo.iter_commits():
65
- if in_bounds(commit.committed_datetime, start, end):
66
- commits.append(commit)
67
- if commits:
68
- results.append({"path": path, "commits": commits})
69
- semaphore.release()
70
- return get_repo_commits
71
-
72
- def in_bounds(value, minimum=None, maximum=None):
73
- return (not minimum or value >= minimum) and (not maximum or value <= maximum)
53
+ def get_commits(semaphore, results, section, username, begin, end, path, token):
54
+ "Gets commits from repo"
55
+ semaphore.acquire()
56
+ if path.exists():
57
+ repo = Repo(path)
58
+ selected_commits = []
59
+ commits = repo.iter_commits(reverse=True)
60
+ first_commit = next(commits)
61
+ for commit in commits:
62
+ if in_bounds(commit.committed_datetime, begin, end):
63
+ selected_commits.append(commit)
64
+ if selected_commits:
65
+ results.append({"path": path, "commits": selected_commits})
66
+ semaphore.release()
74
67
 
75
68
  def format_commit(commit):
76
69
  return '\n'.join([
@@ -9,6 +9,7 @@ from making_with_code_cli.styles import (
9
9
  from making_with_code_cli.teach.setup import check_required_teacher_settings
10
10
  from making_with_code_cli.teach.section.create import create_section
11
11
  from making_with_code_cli.teach.section.edit import edit_section
12
+ from making_with_code_cli.teach.section.show import show_section
12
13
 
13
14
  @click.group(invoke_without_command=True)
14
15
  @click.pass_context
@@ -42,4 +43,5 @@ def summarize_sections(section_data):
42
43
 
43
44
  section.add_command(create_section)
44
45
  section.add_command(edit_section)
46
+ section.add_command(show_section)
45
47
 
@@ -0,0 +1,53 @@
1
+ import click
2
+ from csv import DictWriter
3
+ from making_with_code_cli.settings import read_settings
4
+ from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
5
+ from making_with_code_cli.styles import (
6
+ address,
7
+ error,
8
+ info,
9
+ question,
10
+ )
11
+
12
+ @click.command('show')
13
+ @click.argument("slug")
14
+ @click.option("--config", help="Path to config file (default: ~/.mwc)")
15
+ @click.option("-o", "--outfile", help="Save results as csv")
16
+ def show_section(config, slug, outfile):
17
+ "Show section details"
18
+ settings = read_settings(config)
19
+ api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
20
+ params = {"slug": slug}
21
+ response = api.get_roster(settings.get('mwc_accounts_token'))
22
+ try:
23
+ section = get_section(response, slug)
24
+ except ValueError as err:
25
+ click.echo(error(str(err)))
26
+ if outfile:
27
+ students = section['student_tokens']
28
+ result = [{"username": s, "git_token": t, "section": slug} for s, t in students.items()]
29
+ if not result:
30
+ click.echo(error(f"Nothing to write; there are no students in {slug}."))
31
+ return
32
+ with open(outfile, "w") as fh:
33
+ writer = DictWriter(fh, result[0].keys())
34
+ writer.writeheader()
35
+ writer.writerows(result)
36
+ else:
37
+ click.echo(info(f"name: {section['name']}"))
38
+ click.echo(info(f"slug: {section['slug']}"))
39
+ click.echo(info(f"course: {section['course_name']}"))
40
+ click.echo(info(f"curriculum site: {section['curriculum_site_url']}"))
41
+ click.echo(info(f"join code: {section['code']}"))
42
+ if section['student_tokens']:
43
+ click.echo(info("students:"))
44
+ for student in sorted(section['student_tokens'].keys()):
45
+ click.echo(info(f" - {student}"))
46
+ else:
47
+ click.echo(info("students: []"))
48
+
49
+ def get_section(roster, slug):
50
+ for section in roster['teacher_sections']:
51
+ if section['slug'] == slug:
52
+ return section
53
+ raise ValueError(f"Invalid section {slug}")
@@ -5,6 +5,7 @@ from tabulate import tabulate
5
5
  from making_with_code_cli.settings import read_settings
6
6
  from making_with_code_cli.teach.setup import check_required_teacher_settings
7
7
  from making_with_code_cli.teach.student_repos import StudentRepos
8
+ from making_with_code_cli.helpers import date_string
8
9
  from making_with_code_cli.teach.student_repo_functions import (
9
10
  count_commits,
10
11
  count_changed_py_lines,
@@ -41,8 +42,10 @@ measures = {
41
42
  @click.option("-o", "--outfile", help="Save results as csv")
42
43
  @click.option('-U', "--update", is_flag=True, help="Update repos first")
43
44
  @click.option('-t', "--threads", type=int, default=8, help="Maximum simultaneous threads")
45
+ @click.option('-B', "--begin", type=date_string, help="Begin date")
46
+ @click.option('-C', "--end", type=date_string, help="End date")
44
47
  def status(config, section, course, user, unit, module, measure, anonymous, short,
45
- outfile, update, threads):
48
+ outfile, update, threads, begin, end):
46
49
  "Show status of student repos by module"
47
50
  if update:
48
51
  from making_with_code_cli.teach.update import update as update_task
@@ -52,8 +55,17 @@ def status(config, section, course, user, unit, module, measure, anonymous, shor
52
55
  return
53
56
  repos = StudentRepos(settings, threads)
54
57
  measure_status_message, measure_fn = measures[measure]
55
- results = repos.apply(measure_fn, section=section, course=course, user=user,
56
- unit=unit, module=module, status_message=measure_status_message)
58
+ results = repos.apply(
59
+ measure_fn,
60
+ section=section,
61
+ course=course,
62
+ user=user,
63
+ unit=unit,
64
+ module=module,
65
+ begin=begin,
66
+ end=end,
67
+ status_message=measure_status_message
68
+ )
57
69
  recursive_dict = lambda: defaultdict(recursive_dict)
58
70
  results_dict = recursive_dict()
59
71
  for r in results:
@@ -2,6 +2,7 @@ import os
2
2
  from subprocess import run
3
3
  from git import Repo
4
4
  from making_with_code_cli.teach.gitea_api.api import GiteaTeacherApi
5
+ from making_with_code_cli.helpers import in_bounds
5
6
 
6
7
  MWC_GIT_PROTOCOL = "https"
7
8
  MWC_GIT_SERVER = "git.makingwithcode.org"
@@ -9,7 +10,7 @@ MWC_GIT_SERVER = "git.makingwithcode.org"
9
10
  def run_in_repo(command, cwd):
10
11
  return run(command, cwd=cwd, shell=True, capture_output=True, text=True)
11
12
 
12
- def update_repo(semaphore, results, section, username, path, token):
13
+ def update_repo(semaphore, results, section, username, begin, end, path, token):
13
14
  semaphore.acquire()
14
15
  git_api = GiteaTeacherApi()
15
16
  if path.exists():
@@ -25,31 +26,38 @@ def update_repo(semaphore, results, section, username, path, token):
25
26
  results.append({"action": "clone", "path": path, "process": process})
26
27
  semaphore.release()
27
28
 
28
- def count_commits(semaphore, results, section, username, path, token):
29
+ def count_commits(semaphore, results, section, username, begin, end, path, token):
29
30
  "Counts commits in repo"
30
31
  semaphore.acquire()
31
32
  if path.exists():
32
33
  repo = Repo(path)
34
+ commits = repo.iter_commits(reverse=True)
35
+ first_commit = next(commits)
36
+ n = 0
37
+ for commit in commits:
38
+ if in_bounds(commit.committed_datetime, begin, end):
39
+ n += 1
33
40
  results.append({
34
41
  "section": section,
35
42
  "username": username,
36
43
  "module": path.name,
37
- "score": len(list(repo.iter_commits())) - 1
44
+ "score": n,
38
45
  })
39
46
  semaphore.release()
40
47
 
41
- def count_changed_py_lines(semaphore, results, section, username, path, token):
48
+ def count_changed_py_lines(semaphore, results, section, username, begin, end, path, token):
42
49
  "Counts lines changed in Python files across all commits in repo"
43
50
  semaphore.acquire()
44
51
  if path.exists():
45
52
  repo = Repo(path)
46
53
  changed_lines = 0
47
- commits = repo.iter_commits()
54
+ commits = repo.iter_commits(reverse=True)
48
55
  first_commit = next(commits)
49
56
  for commit in commits:
50
- for f, stats in commit.stats.files.items():
51
- if f.endswith(".py"):
52
- changed_lines += stats['lines']
57
+ if in_bounds(commit.committed_datetime, begin, end):
58
+ for f, stats in commit.stats.files.items():
59
+ if f.endswith(".py"):
60
+ changed_lines += stats['lines']
53
61
  results.append({
54
62
  "section": section,
55
63
  "username": username,
@@ -58,18 +66,19 @@ def count_changed_py_lines(semaphore, results, section, username, path, token):
58
66
  })
59
67
  semaphore.release()
60
68
 
61
- def count_changed_md_lines(semaphore, results, section, username, path, token):
69
+ def count_changed_md_lines(semaphore, results, section, username, begin, end, path, token):
62
70
  "Counts lines changed in Python files across all commits in repo"
63
71
  semaphore.acquire()
64
72
  if path.exists():
65
73
  repo = Repo(path)
66
74
  changed_lines = 0
67
- commits = repo.iter_commits()
75
+ commits = repo.iter_commits(reverse=True)
68
76
  first_commit = next(commits)
69
77
  for commit in commits:
70
- for f, stats in commit.stats.files.items():
71
- if f.endswith(".md"):
72
- changed_lines += stats['lines']
78
+ if in_bounds(commit.committed_datetime, begin, end):
79
+ for f, stats in commit.stats.files.items():
80
+ if f.endswith(".md"):
81
+ changed_lines += stats['lines']
73
82
  results.append({
74
83
  "section": section,
75
84
  "username": username,
@@ -78,7 +87,7 @@ def count_changed_md_lines(semaphore, results, section, username, path, token):
78
87
  })
79
88
  semaphore.release()
80
89
 
81
- def module_completion(semaphore, results, section, username, path, token):
90
+ def module_completion(semaphore, results, section, username, begin, end, path, token):
82
91
  "Returns a [0..1] ratio of module completion, based on tests."
83
92
  semaphore.acquire()
84
93
  if path.exists():
@@ -14,7 +14,7 @@ class StudentRepos:
14
14
  self.max_threads = max_threads
15
15
 
16
16
  def apply(self, function, section=None, course=None, user=None,
17
- unit=None, module=None, status_message=""):
17
+ unit=None, module=None, begin=None, end=None, status_message=""):
18
18
  """Applies function to repo paths matching args, and returns the result.
19
19
  The function should take (sem, result, g, user, path, token).
20
20
  Application is in parallel, using up to max_threads.
@@ -23,7 +23,7 @@ class StudentRepos:
23
23
  results = []
24
24
  threads = []
25
25
  for g, user, path, token in self.iter_repos(section, course, user, unit, module):
26
- thread = Thread(target=function, args=(sem, results, g, user, path, token))
26
+ thread = Thread(target=function, args=(sem, results, g, user, begin, end, path, token))
27
27
  thread.start()
28
28
  threads.append(thread)
29
29
  for i, thread in enumerate(threads):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "making-with-code-cli"
3
- version = "4.0.0"
3
+ version = "4.1.0"
4
4
  description = "Courseware for Making With Code"
5
5
  authors = [
6
6
  {name = "Chris Proctor",email = "chris@chrisproctor.net"}