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.
- collect_homework-0.1.0/.gitignore +3 -0
- collect_homework-0.1.0/.gitignore~ +1 -0
- collect_homework-0.1.0/Makefile.pypi +38 -0
- collect_homework-0.1.0/PKG-INFO +64 -0
- collect_homework-0.1.0/README.md +51 -0
- collect_homework-0.1.0/collect_homework/cli.py +274 -0
- collect_homework-0.1.0/collect_homework/parallel_shells.py +66 -0
- collect_homework-0.1.0/pyproject.toml +38 -0
|
@@ -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"
|