collect-homework 0.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.
@@ -0,0 +1,3 @@
1
+ __pycache__
2
+ test-folder
3
+
@@ -0,0 +1 @@
1
+ **/__pycache__
@@ -0,0 +1,38 @@
1
+ # derived from name in setup.py
2
+
3
+ PYPI_NAME = $(shell python setup.py --name)
4
+ VERSION = $(shell python setup.py --version)
5
+
6
+ ########## for uploading onto pypi
7
+ # updated in May 2020 to use twine for uploads
8
+ # run pip install twine if needed
9
+ # to initialize twine credentials
10
+ # keyring set upload.pypi.org parmentelat
11
+ # keyring set test.pypi.org parmentelat
12
+
13
+ VERSIONTAG = $(PYPI_NAME)-$(VERSION)
14
+ GIT-TAG-ALREADY-SET = $(shell git tag | grep '^$(VERSIONTAG)$$')
15
+ # to check for uncommitted changes
16
+ GIT-CHANGES = $(shell echo $$(git diff HEAD | wc -l))
17
+
18
+ # run this only once the sources are in on the right tag
19
+ pypi: twine cleanpypi
20
+ @if [ $(GIT-CHANGES) != 0 ]; then echo "You have uncommitted changes - cannot publish"; false; fi
21
+ @if [ -n "$(GIT-TAG-ALREADY-SET)" ] ; then echo "tag $(VERSIONTAG) already set"; false; fi
22
+ @if ! grep -q ' $(VERSION)' CHANGELOG.md ; then echo no mention of $(VERSION) in CHANGELOG.md; false; fi
23
+ @echo "You are about to release $(VERSION) - OK (Ctrl-c if not) ? " ; read _
24
+ git tag $(VERSIONTAG)
25
+ ./setup.py sdist bdist_wheel
26
+ twine upload dist/*
27
+
28
+ twine:
29
+ @type -p twine >& /dev/null || { echo ==== installing twine; pip install twine; }
30
+
31
+ testpypi: cleanpypi
32
+ ./setup.py sdist bdist_wheel
33
+ twine upload --repository-url https://test.pypi.org/legacy/ dist/*
34
+
35
+ cleanpypi:
36
+ rm -rf build dist
37
+
38
+ .PHONY: pypi testpypi cleanpypi twine
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.3
2
+ Name: collect-homework
3
+ Version: 0.1.0
4
+ Summary: Easily manage homework submissions
5
+ Project-URL: Repository, https://github.com/flotpython/collect-homework
6
+ Project-URL: Bug Tracker, https://github.com/flotpython/collect-homework/issues
7
+ Author-email: Thierry Parmentelat <thierry.parmentelat@minesparis.psl.eu>
8
+ License: MIT License
9
+ Keywords: github,homework,submissions
10
+ Requires-Python: >=3.8
11
+ Requires-Dist: click
12
+ Description-Content-Type: text/markdown
13
+
14
+ # manage your student's homework
15
+
16
+ ## Description
17
+
18
+ assuming you have asked your students to create a repo on github to submit their homework,
19
+ this script will help you to clone all the repos in a folder,
20
+ and will help you fetch/pull/merge all the repos in the folder at once
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ pip install collect-homework
26
+ ```
27
+
28
+ ## Initial setup
29
+
30
+ 1. create a folder where you want to clone all the repos
31
+ 1. create a file named `00.ids` in the folder
32
+ this file should contain the github ids of all the students, one id per line
33
+ 1. at that point
34
+ ```bash
35
+ collect-homework clone
36
+ ```
37
+ will clone all the repos in the folder
38
+ 1. in order to pull all the repos in the folder
39
+ ```bash
40
+ collect-homework pull
41
+ ```
42
+ 1. you get the gist of it; type
43
+ ```bash
44
+ collect-homework help
45
+ ```
46
+ to see all the available commands
47
+ 1. of particular interest is the `collect-homework summary` command,
48
+ which will display the status of all the repos in the folder
49
+ 1. finally you can focus on one or several students by typing e.g. (this makes sense for most commands)
50
+ ```bash
51
+ collect-homework -s "JohnDoe JaneMartin" pull
52
+ ```
53
+ and then you can pull/merge/fetch only the repos of the students you have focused on
54
+
55
+ ## handling repo names
56
+
57
+ assuming you run this command in a folder e.g. `/Users/johndoe/python-homework`:
58
+
59
+ - then by default all the students are expected to have a repo named `python-homework`
60
+ - if this does not match your setup, you can specify the repo name in the `00.reponame` file
61
+ this one-line file should contain the name of all the repos, e.g. `python-homework-2021`
62
+ - and if a student does not comply with that name, you can specify the repo name in the `00.repos`
63
+ file by writing `student/repo` instead of just `student`
64
+ (you can even put the complete github url instead if that's more convenient for you)
@@ -0,0 +1,51 @@
1
+ # manage your student's homework
2
+
3
+ ## Description
4
+
5
+ assuming you have asked your students to create a repo on github to submit their homework,
6
+ this script will help you to clone all the repos in a folder,
7
+ and will help you fetch/pull/merge all the repos in the folder at once
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install collect-homework
13
+ ```
14
+
15
+ ## Initial setup
16
+
17
+ 1. create a folder where you want to clone all the repos
18
+ 1. create a file named `00.ids` in the folder
19
+ this file should contain the github ids of all the students, one id per line
20
+ 1. at that point
21
+ ```bash
22
+ collect-homework clone
23
+ ```
24
+ will clone all the repos in the folder
25
+ 1. in order to pull all the repos in the folder
26
+ ```bash
27
+ collect-homework pull
28
+ ```
29
+ 1. you get the gist of it; type
30
+ ```bash
31
+ collect-homework help
32
+ ```
33
+ to see all the available commands
34
+ 1. of particular interest is the `collect-homework summary` command,
35
+ which will display the status of all the repos in the folder
36
+ 1. finally you can focus on one or several students by typing e.g. (this makes sense for most commands)
37
+ ```bash
38
+ collect-homework -s "JohnDoe JaneMartin" pull
39
+ ```
40
+ and then you can pull/merge/fetch only the repos of the students you have focused on
41
+
42
+ ## handling repo names
43
+
44
+ assuming you run this command in a folder e.g. `/Users/johndoe/python-homework`:
45
+
46
+ - then by default all the students are expected to have a repo named `python-homework`
47
+ - if this does not match your setup, you can specify the repo name in the `00.reponame` file
48
+ this one-line file should contain the name of all the repos, e.g. `python-homework-2021`
49
+ - and if a student does not comply with that name, you can specify the repo name in the `00.repos`
50
+ file by writing `student/repo` instead of just `student`
51
+ (you can even put the complete github url instead if that's more convenient for you)
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env python
2
+
3
+ """
4
+ Description: a script to manage a collection of git repositories,
5
+ with one folder per student
6
+
7
+ \b
8
+ Config files:
9
+ 00.ids: where to store the students ids (and optionally reponames)
10
+ 00.reponame (optional): how the student repo is supposed to be named
11
+
12
+ \b
13
+ Workflow:
14
+ 0. you start with creating a file `00.ids` containing the github slugs (ids) of the students
15
+ all the ones that you are aware of at least
16
+ 1. ask each student to create a github repo named like e.g. numerique-homework
17
+ usually repo should be private (so students can't see each other's work),
18
+ in this case, they invite you, the teacher, as a collaborator
19
+ 2. beware to accept their invitation in a timely manner, as a github
20
+ invitation expires after typically 7 days
21
+ 3. create a fresh folder, named after the target reponame
22
+ so typically you could put all this in a folder
23
+ $HOME/git/ue12-p23-numerique/numerique-homework
24
+ alternately, if this convention does not suit you, you can instead
25
+ create a file named `00.reponame` containing the name of the repo
26
+ (e.g. numerique-homework)
27
+ this will be used to locate the student repos to be cloned
28
+ 4. as you receive new invitations, add them to a file named 00.ids if missing
29
+ see below for details on this file format
30
+ 5. run this script to clone all the repos with
31
+ collect-homework clone
32
+ this can be done several times, the existing folders will be skipped
33
+ so only the missing students are tried again
34
+ 6. if a student has misspelled their repo name
35
+ e.g. it is called homework-num instead of the expected numerique-homework
36
+ then tweak 00.ids (see the format below)
37
+ 7. iterate until you have all the slugs and all the repos cloned
38
+
39
+ in the meanwhile, you can use all the functions below
40
+
41
+ collect-homework --help
42
+
43
+ \b
44
+ collect-homework clone will clone all repos
45
+ collect-homework summary will output a summary of all repos (nb of commits, last commit date, ...)
46
+ collect-homework slugs will output all slugs on stdout
47
+ collect-homework repos will output all ids on stdout
48
+ collect-homework fetch will fetch all repos
49
+ collect-homework pull will pull all repos
50
+ collect-homework log1 will show the (full) last commit of each repo
51
+ collect-homework l1 will show the last commit of each repo (one line per commit)
52
+ collect-homework ln 4 same on the 4 last commits of each repo
53
+ collect-homework missing will output the folders missing
54
+ i.e. students that have not yet created their repo
55
+
56
+ you can use the -s option to restrict to one or several students, like so
57
+
58
+ collect-homework -s "bobarteam, john/ albert"
59
+
60
+ the -s option expects exactly one argument, it will then use
61
+ either space, comma or slash to cut the student names
62
+
63
+ Format for 00.ids:
64
+
65
+ \b
66
+ 00.ids is supposed to contain, one per line, the github ids of the students
67
+ it can contain comments, starting with a #
68
+ for each student you can use any of the following formats
69
+ - https://github.com/student/repo
70
+ - student/repo
71
+ - student
72
+
73
+ when the repo is not provided, it is assumed to be
74
+ either the content of 00.reponame if that file exists
75
+ otherwise the name of the current folder
76
+ """
77
+
78
+ import sys
79
+ from pathlib import Path
80
+ import re
81
+ import subprocess
82
+
83
+ import click
84
+
85
+ from collect_homework.parallel_shells import ParallelShells
86
+
87
+
88
+ CFG_REPONAME = "00.reponame"
89
+ CFG_SLUGS = "00.ids"
90
+
91
+
92
+ class Init:
93
+ """
94
+ reads current status
95
+ """
96
+
97
+ @staticmethod
98
+ def get_reponame():
99
+ cfg_reponame = Path(CFG_REPONAME)
100
+ if not cfg_reponame.exists():
101
+ return Path(".").absolute().parts[-1]
102
+ with cfg_reponame.open() as f:
103
+ return f.read().strip()
104
+
105
+ @staticmethod
106
+ def get_slugs(default_reponame):
107
+ cfg_slugs = Path(CFG_SLUGS)
108
+ result = {}
109
+ with cfg_slugs.open() as f:
110
+ for line in f:
111
+ if line.startswith('#'):
112
+ continue
113
+ line = line.strip()
114
+ if "/" in line:
115
+ slug, reponame = line.split("/")
116
+ slug = slug.replace("https://github.com/", "")
117
+ else:
118
+ slug = line
119
+ reponame = default_reponame
120
+ result[slug] = reponame
121
+ return dict(sorted(result.items(), key=lambda x: x[0].lower()))
122
+
123
+ @staticmethod
124
+ def get_actual_repos():
125
+ for dir in Path(".").glob("*/.git"):
126
+ yield dir.parent.parts[-1]
127
+
128
+
129
+ # globals
130
+
131
+ REPONAME = Init.get_reponame()
132
+ SLUGS = Init.get_slugs(REPONAME)
133
+ DIRS = list(Init.get_actual_repos())
134
+
135
+
136
+
137
+ def _git_proxy(message, *git_command):
138
+ """
139
+ Run a git command on all DIRS
140
+ """
141
+ for slug in Init.get_actual_repos():
142
+ if slug not in SLUGS:
143
+ continue
144
+ if message:
145
+ print(f"===== {message} in {slug}")
146
+ command = ["git", "-C", slug, *git_command]
147
+ # print(command)
148
+ subprocess.run(command)
149
+
150
+ # message not used yet
151
+ def _git_proxy_parallel(_message, *git_command):
152
+ """
153
+ Run a git command on all DIRS, but asynchronously
154
+ """
155
+ commands = []
156
+ for slug in Init.get_actual_repos():
157
+ if slug not in SLUGS:
158
+ continue
159
+ command = f"git -C {slug} {' '.join(git_command)}"
160
+ commands.append(command)
161
+ ParallelShells(commands).run()
162
+
163
+ # the click CLI object
164
+ @click.group(chain=True, help=sys.modules[__name__].__doc__)
165
+ @click.option('-s', '--students', help="comma-separated")
166
+ def cli(students):
167
+ if students is None:
168
+ return
169
+ global SLUGS
170
+ focus_slugs = {}
171
+ students = re.split(r'[/,\s]+', students)
172
+ for student in students:
173
+ if not student:
174
+ continue
175
+ if student not in SLUGS:
176
+ print(f"unknown student {student} in {CFG_SLUGS} - -s option ignored")
177
+ return
178
+ focus_slugs[student] = SLUGS[student]
179
+ SLUGS = focus_slugs
180
+
181
+
182
+ # commands
183
+ @cli.command('slugs')
184
+ @click.option('-v', '--verbose', is_flag=True, default=False, help="show reponames", )
185
+ def slugs(verbose):
186
+ for slug, reponame in SLUGS.items():
187
+ print(f"{slug}/{reponame}" if verbose else slug)
188
+
189
+ @cli.command('dirs')
190
+ def dirs():
191
+ for dir in sorted(DIRS, key=lambda x: x.lower()):
192
+ print(dir)
193
+
194
+ @cli.command('missing')
195
+ def missing():
196
+ for slug, reponame in SLUGS.items():
197
+ if slug not in DIRS:
198
+ print(slug)
199
+
200
+
201
+ @cli.command('clone')
202
+ def clone():
203
+ commands = []
204
+ for slug, reponame in SLUGS.items():
205
+ gitrepo = Path(slug) / "git"
206
+ if gitrepo.exists() and gitrepo.is_dir():
207
+ print(f"{slug} OK")
208
+ continue
209
+ commands.append(["git", "clone", "git@github.com:{slug}/{reponame}.git", slug])
210
+ ParallelShells(commands).run()
211
+
212
+
213
+ @cli.command('urls')
214
+ def urls(): _git_proxy("", "remote", "get-url", "origin")
215
+ @cli.command('status')
216
+ def status(): _git_proxy("STATUS", "status")
217
+ @cli.command('s')
218
+ def s(): _git_proxy("SHORT STATUS", "status", "--short", "--untracked-files=no")
219
+ @cli.command('diff')
220
+ def diff(): _git_proxy("DIFF", "diff")
221
+ @cli.command('pull')
222
+ def pull(): _git_proxy_parallel("PULL", "pull")
223
+ @cli.command('fetch')
224
+ def fetch(): _git_proxy_parallel("FETCH", "fetch")
225
+ @cli.command('reset')
226
+ def reset(): _git_proxy("RESET", "reset", "--hard")
227
+ @cli.command('merge')
228
+ def merge(): _git_proxy("MERGE", "merge", "origin/main")
229
+
230
+ FORMAT = """--format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(dim bold red)%an%C(reset) - %s%C(red)%d%C(reset)'"""
231
+
232
+ @cli.command('l1')
233
+ def l1(): _git_proxy("", "log", "-1", FORMAT )
234
+ @cli.command('log1')
235
+ def log1(): _git_proxy("last commit", "log", "-1")
236
+ @cli.command('l5')
237
+ def l5(): _git_proxy( "LISTING 5 latest commits", "log", "--oneline", "--all", "--graph", "-5", FORMAT )
238
+
239
+ @cli.command('ln')
240
+ @click.argument('n', type=int)
241
+ def ln(n): _git_proxy( f"LISTING {n} latest commits", "log", "--oneline", "--all", "--graph", f"-{n}", FORMAT )
242
+
243
+
244
+ # pour a bit of shell in the mix
245
+ summary_function = """
246
+ function summary() {
247
+ local slug="$1"; shift
248
+ [[ -d $slug ]] || { printf '%27s ' $slug; echo MISSING; return; }
249
+ git -C $slug log >& /dev/null || { printf '%27s ' $slug; echo EMPTY; return; }
250
+ printf '%24s ' $slug
251
+ local nb_folders=$(find $slug -type d -depth +0 | grep -vE '/.git($|/)' | wc -l)
252
+ local nb_files=$(find $slug -type f | fgrep -v '/.git/' | wc -l)
253
+ local nb_git_files=$(git -C $slug ls-files | wc -l)
254
+ local nb_commits=$(git -C $slug log --oneline | wc -l)
255
+ local dateh=$(git -C $slug log -1 --format='%ah')
256
+ local date=$(git -C $slug log -1 --format='%ar')
257
+ printf "%2d folders - " $nb_folders
258
+ printf "%3d files - " $nb_files
259
+ printf "%3d git files - " $nb_git_files
260
+ printf "%3d commits - " $nb_commits
261
+ printf "last on %s (%s)" "$dateh" "$date"
262
+ echo
263
+ }
264
+ """
265
+
266
+ @cli.command('summary')
267
+ def summary():
268
+ commands = []
269
+ for slug, _ in SLUGS.items():
270
+ commands.append(summary_function + f"summary {slug}")
271
+ ParallelShells(commands, echo=False).run()
272
+
273
+ if __name__ == '__main__':
274
+ cli()
@@ -0,0 +1,66 @@
1
+ """
2
+ a utility to run several commands in parallel
3
+ and show the results in order once they're all done
4
+ """
5
+
6
+ from pathlib import Path
7
+ import asyncio
8
+
9
+ class ParallelShells:
10
+
11
+ def __init__(self, commands: list[str], echo=True):
12
+ self.commands = commands
13
+ self.echo = echo
14
+
15
+ def run(self):
16
+ """
17
+ a synchronous function that will run all
18
+ subcommands in parallel and show all the results
19
+ sequentially once they're all done
20
+ """
21
+ return asyncio.run(self._run())
22
+
23
+ async def _run(self):
24
+ """
25
+ an async function that will run all
26
+ subcommands in parallel and show all the results
27
+ sequentially once they're all done
28
+ """
29
+ tasks = []
30
+ for command in self.commands:
31
+ tasks.append(asyncio.create_task(self._run_command(command)))
32
+ for task in tasks:
33
+ await task
34
+
35
+ async def _run_command(self, command):
36
+ """
37
+ an async function that will run a single command
38
+ """
39
+ proc = await asyncio.create_subprocess_shell(
40
+ command,
41
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
42
+ stdout, stderr = await proc.communicate()
43
+ if self.echo:
44
+ print(f"===> {command}")
45
+ if stdout:
46
+ if stderr: # if no stderr, no need to separate
47
+ print("[stdout]:")
48
+ print(stdout.decode(), end="")
49
+ if stderr:
50
+ print("[stderr]:")
51
+ print(stderr.decode(), end="")
52
+
53
+ if __name__ == "__main__":
54
+ commands = [
55
+ "ls -l",
56
+ "echo the sleeper; sleep 1", # this one will be the last to finish
57
+ "echo hello",
58
+ "ls -l /tmp",
59
+ """function foo() {
60
+ echo "this is a function"
61
+ [ -f /etc/passwd ] && echo "passwd exists"
62
+ }
63
+ foo
64
+ """
65
+ ]
66
+ ParallelSh(commands).run()
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "collect-homework"
8
+ version = "0.1.0"
9
+ dependencies = [ "click", ]
10
+ requires-python = ">=3.8"
11
+ readme = "README.md"
12
+ license = {text = "MIT License"}
13
+ description = "Easily manage homework submissions"
14
+ keywords = ["github", "homework", "submissions"]
15
+ authors = [
16
+ {name = "Thierry Parmentelat", email = "thierry.parmentelat@minesparis.psl.eu"},
17
+ ]
18
+
19
+ # [project.optional-dependencies]
20
+ # gui = ["PyQt5"]
21
+ # cli = [
22
+ # "click",
23
+ # ]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = [ "collect_homework" ]
27
+
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/flotpython/collect-homework"
31
+ "Bug Tracker" = "https://github.com/flotpython/collect-homework/issues"
32
+ # Homepage = "https://example.com"
33
+ # Documentation = "https://readthedocs.org"
34
+ # Changelog = "https://github.com/me/spam/blob/master/CHANGELOG.md"
35
+
36
+
37
+ [project.scripts]
38
+ collect-homework = "collect_homework.cli:cli"