python-interactive-courses 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyics/__init__.py +0 -0
- pyics/app/actions.py +179 -0
- pyics/app/cli.py +335 -0
- pyics/app/key_listener.py +133 -0
- pyics/app/prompts.py +439 -0
- pyics/app/pyics.py +454 -0
- pyics/data/__init__.py +4 -0
- pyics/data/course.py +531 -0
- pyics/data/task.py +744 -0
- pyics/distribution/__init__.py +5 -0
- pyics/distribution/archive.py +667 -0
- pyics/distribution/dependencies.py +452 -0
- pyics/distribution/fetch.py +128 -0
- pyics/distribution/pull.py +376 -0
- pyics/distribution/slugs.py +24 -0
- pyics/distribution/sources.py +561 -0
- pyics/notebook/__init__.py +1 -0
- pyics/notebook/cells.py +184 -0
- pyics/notebook/loader.py +590 -0
- pyics/notebook/sections.py +189 -0
- pyics/plugins.py +90 -0
- pyics/scripts/console.py +86 -0
- pyics/scripts/create_course.py +301 -0
- pyics/scripts/create_task.py +850 -0
- pyics/scripts/edit_course.py +133 -0
- pyics/scripts/edit_task.py +109 -0
- pyics/scripts/main.py +118 -0
- pyics/scripts/pack.py +198 -0
- pyics/scripts/pull.py +123 -0
- pyics/scripts/sources.py +118 -0
- pyics/tests/__init__.py +3 -0
- pyics/tests/capture.py +114 -0
- pyics/tests/check_specs.py +528 -0
- pyics/tests/check_structure.py +769 -0
- pyics/tests/compare.py +184 -0
- pyics/tests/pytest_integration.py +228 -0
- pyics/tests/results.py +145 -0
- pyics/tests/tests.py +440 -0
- pyics/tests/trace.py +156 -0
- pyics/util/discovery.py +92 -0
- pyics/util/json_handling.py +182 -0
- pyics/util/logging.py +39 -0
- pyics/util/paths.py +38 -0
- pyics/util/terminal.py +57 -0
- python_interactive_courses-1.0.0.dist-info/METADATA +245 -0
- python_interactive_courses-1.0.0.dist-info/RECORD +49 -0
- python_interactive_courses-1.0.0.dist-info/WHEEL +4 -0
- python_interactive_courses-1.0.0.dist-info/entry_points.txt +3 -0
- python_interactive_courses-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from datetime import date, time, datetime, timedelta
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import json_tricks
|
|
11
|
+
from json_tricks.decoders import pathlib_hook
|
|
12
|
+
|
|
13
|
+
from pyics.util.paths import StrPath
|
|
14
|
+
|
|
15
|
+
_OPAQUE_TYPES = (
|
|
16
|
+
str,
|
|
17
|
+
bytes,
|
|
18
|
+
int,
|
|
19
|
+
float,
|
|
20
|
+
bool,
|
|
21
|
+
type(None),
|
|
22
|
+
Decimal,
|
|
23
|
+
Fraction,
|
|
24
|
+
date,
|
|
25
|
+
time,
|
|
26
|
+
datetime,
|
|
27
|
+
timedelta,
|
|
28
|
+
Enum,
|
|
29
|
+
complex,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _tag_tuples(obj):
|
|
34
|
+
if isinstance(obj, tuple):
|
|
35
|
+
return {"__tuple__": [_tag_tuples(v) for v in obj]}
|
|
36
|
+
if isinstance(obj, (set, frozenset)):
|
|
37
|
+
kind = "__pyics_frozenset__" if isinstance(obj, frozenset) else "__pyics_set__"
|
|
38
|
+
try:
|
|
39
|
+
items = sorted(obj)
|
|
40
|
+
except TypeError:
|
|
41
|
+
items = list(obj)
|
|
42
|
+
return {kind: [_tag_tuples(v) for v in items]}
|
|
43
|
+
if isinstance(obj, Path):
|
|
44
|
+
# json_tricks would store str(obj), i.e. backslashes on Windows. Forward slashes
|
|
45
|
+
# are read correctly on all platforms.
|
|
46
|
+
return {"__pathlib__": obj.as_posix()}
|
|
47
|
+
if isinstance(obj, list):
|
|
48
|
+
return [_tag_tuples(v) for v in obj]
|
|
49
|
+
if isinstance(obj, dict):
|
|
50
|
+
return {k: _tag_tuples(v) for k, v in obj.items()}
|
|
51
|
+
if isinstance(obj, _OPAQUE_TYPES):
|
|
52
|
+
return obj
|
|
53
|
+
if hasattr(obj, "__dict__"):
|
|
54
|
+
new_obj = copy.copy(obj)
|
|
55
|
+
object.__setattr__(
|
|
56
|
+
new_obj, "__dict__", {k: _tag_tuples(v) for k, v in obj.__dict__.items()}
|
|
57
|
+
)
|
|
58
|
+
return new_obj
|
|
59
|
+
return obj
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _untag_tuples(dct):
|
|
63
|
+
if isinstance(dct, dict):
|
|
64
|
+
if "__tuple__" in dct:
|
|
65
|
+
return tuple(dct["__tuple__"])
|
|
66
|
+
if "__pyics_set__" in dct:
|
|
67
|
+
return set(dct["__pyics_set__"])
|
|
68
|
+
if "__pyics_frozenset__" in dct:
|
|
69
|
+
return frozenset(dct["__pyics_frozenset__"])
|
|
70
|
+
return dct
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def dumps(obj: Any, **kw: Any) -> str:
|
|
74
|
+
"""Wrapper around ``json_tricks.dumps`` to tag tuples early. Compression isn't
|
|
75
|
+
supported, since its result is bytes."""
|
|
76
|
+
txt = json_tricks.dumps(_tag_tuples(obj), **kw)
|
|
77
|
+
if not isinstance(txt, str):
|
|
78
|
+
raise TypeError("compressed output isn't supported, use json_tricks directly")
|
|
79
|
+
return txt
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def dump(obj, fp, **kw):
|
|
83
|
+
"""Wrapper around ``json_tricks.dump`` to tag tuples early."""
|
|
84
|
+
return json_tricks.dump(_tag_tuples(obj), fp, **kw)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def loads(s, **kw):
|
|
88
|
+
"""Wrapper around ``json_tricks.loads`` to automatically add the _untag_tuples hook."""
|
|
89
|
+
extra_hooks = (_untag_tuples,) + kw.get("extra_obj_pairs_hooks", tuple())
|
|
90
|
+
return json_tricks.loads(s, extra_obj_pairs_hooks=extra_hooks, **kw)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load(fp, **kw):
|
|
94
|
+
"""Wrapper around ``json_tricks.load`` to automatically add the _untag_tuples hook."""
|
|
95
|
+
extra_hooks = (_untag_tuples,) + kw.get("extra_obj_pairs_hooks", tuple())
|
|
96
|
+
return json_tricks.load(fp, extra_obj_pairs_hooks=extra_hooks, **kw)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _plain_hook(dct: Any) -> Any:
|
|
100
|
+
"""Decode the tags of paths, tuples and sets, if they are all a dict holds, so that
|
|
101
|
+
nothing that is decoded hides other data from ``scan_tags``."""
|
|
102
|
+
if not isinstance(dct, dict) or len(dct) != 1:
|
|
103
|
+
return dct
|
|
104
|
+
key, value = next(iter(dct.items()))
|
|
105
|
+
if key == "__pathlib__" and isinstance(value, str):
|
|
106
|
+
return pathlib_hook(dct)
|
|
107
|
+
if key in ("__tuple__", "__pyics_set__", "__pyics_frozenset__"):
|
|
108
|
+
if isinstance(value, list):
|
|
109
|
+
try:
|
|
110
|
+
return _untag_tuples(dct)
|
|
111
|
+
except TypeError: # unhashable items of a set
|
|
112
|
+
pass
|
|
113
|
+
return dct
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def loads_plain(s: str) -> Any:
|
|
117
|
+
"""
|
|
118
|
+
Decode ``s`` as plain data: dicts, lists, strings and numbers, paths, and tuples and
|
|
119
|
+
sets as tagged by ``dumps``. Nothing else is decoded, e.g. class instances stay the
|
|
120
|
+
dicts json_tricks tagged them as, so that decoding never imports a module or
|
|
121
|
+
creates an object. For files of plain data, like course manifests, and to inspect a
|
|
122
|
+
file before it is decoded (see ``scan_tags``).
|
|
123
|
+
"""
|
|
124
|
+
return json_tricks.loads(s, obj_pairs_hooks=(_plain_hook,))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_plain(fp: StrPath) -> Any:
|
|
128
|
+
"""``loads_plain`` the file at ``fp``."""
|
|
129
|
+
with open(fp, encoding="utf-8") as f:
|
|
130
|
+
return loads_plain(f.read())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class Tags:
|
|
135
|
+
"""What decoding data tagged by json_tricks needs (see ``scan_tags``)."""
|
|
136
|
+
|
|
137
|
+
# the modules that json_tricks imports to decode the classes and enums named by the
|
|
138
|
+
# tags, None for one defined in a script (i.e. in ``__main__``)
|
|
139
|
+
modules: frozenset[str | None]
|
|
140
|
+
# the packages that json_tricks needs for its own tags: numpy for arrays, pandas
|
|
141
|
+
# for data frames and series, pytz for (date)times with a time zone
|
|
142
|
+
packages: frozenset[str]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def scan_tags(data: Any) -> Tags:
|
|
146
|
+
"""
|
|
147
|
+
Find what decoding ``data``, as read by ``loads_plain``, needs besides the standard
|
|
148
|
+
library: the modules of the classes and enums whose instances it holds, and the
|
|
149
|
+
packages json_tricks needs for some of its tags.
|
|
150
|
+
"""
|
|
151
|
+
modules: set[str | None] = set()
|
|
152
|
+
packages: set[str] = set()
|
|
153
|
+
|
|
154
|
+
def walk(obj: Any) -> None:
|
|
155
|
+
if isinstance(obj, dict):
|
|
156
|
+
if "__instance_type__" in obj:
|
|
157
|
+
modules.add(_tagged_module(obj["__instance_type__"]))
|
|
158
|
+
enum = obj.get("__enum__")
|
|
159
|
+
if isinstance(enum, dict) and "__enum_instance_type__" in enum:
|
|
160
|
+
modules.add(_tagged_module(enum["__enum_instance_type__"]))
|
|
161
|
+
if "__ndarray__" in obj:
|
|
162
|
+
packages.add("numpy")
|
|
163
|
+
if "__pandas_dataframe__" in obj or "__pandas_series__" in obj:
|
|
164
|
+
packages.update(("pandas", "numpy"))
|
|
165
|
+
if ("__datetime__" in obj or "__time__" in obj) and "tzinfo" in obj:
|
|
166
|
+
packages.add("pytz")
|
|
167
|
+
for value in obj.values():
|
|
168
|
+
walk(value)
|
|
169
|
+
elif isinstance(obj, (list, tuple, set, frozenset)):
|
|
170
|
+
for value in obj:
|
|
171
|
+
walk(value)
|
|
172
|
+
|
|
173
|
+
walk(data)
|
|
174
|
+
return Tags(frozenset(modules), frozenset(packages))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _tagged_module(instance_type: Any) -> str | None:
|
|
178
|
+
"""The module of a class tagged as ``[module, name]``."""
|
|
179
|
+
if isinstance(instance_type, (list, tuple)) and instance_type:
|
|
180
|
+
module = instance_type[0]
|
|
181
|
+
return module if isinstance(module, str) and module else None
|
|
182
|
+
return None
|
pyics/util/logging.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
_setup_done: bool = False
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def setup_logging(
|
|
9
|
+
level: logging._Level = logging.INFO, fp: PathLike = Path("./.pyics.log")
|
|
10
|
+
):
|
|
11
|
+
"""
|
|
12
|
+
Perform logging setup
|
|
13
|
+
"""
|
|
14
|
+
global _setup_done
|
|
15
|
+
|
|
16
|
+
logging.root.setLevel(level)
|
|
17
|
+
logging.root.handlers.clear()
|
|
18
|
+
|
|
19
|
+
fp = fp if isinstance(fp, Path) else Path(fp)
|
|
20
|
+
fp.parent.mkdir(exist_ok=True)
|
|
21
|
+
|
|
22
|
+
file_handler = logging.FileHandler(fp, delay=True)
|
|
23
|
+
file_handler.setFormatter(
|
|
24
|
+
logging.Formatter(
|
|
25
|
+
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s (%(lineno)d)",
|
|
26
|
+
datefmt="%H:%M:%S",
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
logging.root.addHandler(file_handler)
|
|
31
|
+
|
|
32
|
+
_setup_done = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_logger(name: str) -> logging.Logger:
|
|
36
|
+
if not _setup_done:
|
|
37
|
+
setup_logging()
|
|
38
|
+
|
|
39
|
+
return logging.getLogger(name)
|
pyics/util/paths.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from os import PathLike
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# a path given as a string or path object, as accepted by ``open`` or ``Path``
|
|
6
|
+
type StrPath = str | PathLike[str]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def relative_to(path: StrPath, base: StrPath) -> Path:
|
|
10
|
+
"""
|
|
11
|
+
Express ``path`` relative to the directory ``base`` (e.g. ``../ex/a.py``), so that it
|
|
12
|
+
can be stored in a file located in ``base``. Relative inputs are interpreted relative
|
|
13
|
+
to the working directory.
|
|
14
|
+
|
|
15
|
+
If no relative path exists (on Windows, ``path`` and ``base`` on different drives),
|
|
16
|
+
returns ``path`` as an absolute path.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
return Path(os.path.relpath(Path(path).absolute(), Path(base).absolute()))
|
|
20
|
+
except ValueError:
|
|
21
|
+
return Path(path).absolute()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve(path: StrPath, base: StrPath) -> Path:
|
|
25
|
+
"""
|
|
26
|
+
Inverse of ``relative_to``: the path ``path`` points to when read from a file located
|
|
27
|
+
in the directory ``base``, normalized (``a/../b`` becomes ``b``). Absolute paths are
|
|
28
|
+
kept as they are.
|
|
29
|
+
"""
|
|
30
|
+
return Path(os.path.normpath(Path(base) / path))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def absolute(path: StrPath) -> Path:
|
|
34
|
+
"""
|
|
35
|
+
``path`` as an absolute, normalized path (``a/../b`` becomes ``b``), so that paths
|
|
36
|
+
given relative to different directories can be compared.
|
|
37
|
+
"""
|
|
38
|
+
return Path(os.path.abspath(path))
|
pyics/util/terminal.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal handling that works the same on all platforms: the escape codes the interface is
|
|
3
|
+
drawn with, the alternate screen it is drawn on, and the terminal's mode.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from prompt_toolkit.input import create_input
|
|
11
|
+
|
|
12
|
+
LINE_UP = "\033[1A"
|
|
13
|
+
LINE_CLEAR = "\x1b[2K"
|
|
14
|
+
HIDE_CURSOR = "\033[?25l"
|
|
15
|
+
SHOW_CURSOR = "\033[?25h"
|
|
16
|
+
ALT_SCREEN_ENTER = "\033[?1049h"
|
|
17
|
+
ALT_SCREEN_LEAVE = "\033[?1049l"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def fullscreen() -> Generator[None]:
|
|
22
|
+
"""
|
|
23
|
+
Draw on the alternate screen while in this context. The cursor is shown again when
|
|
24
|
+
it is left.
|
|
25
|
+
|
|
26
|
+
On Windows, the console only processes escape codes once that is switched on, which
|
|
27
|
+
is done first. ``rich`` checks for it when a ``Console`` is created, so the ``CLI``
|
|
28
|
+
has to be created inside this context.
|
|
29
|
+
"""
|
|
30
|
+
if sys.platform == "win32":
|
|
31
|
+
import colorama
|
|
32
|
+
|
|
33
|
+
colorama.just_fix_windows_console()
|
|
34
|
+
|
|
35
|
+
sys.stdout.write(ALT_SCREEN_ENTER)
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
try:
|
|
38
|
+
yield
|
|
39
|
+
finally:
|
|
40
|
+
sys.stdout.write(SHOW_CURSOR)
|
|
41
|
+
sys.stdout.write(ALT_SCREEN_LEAVE)
|
|
42
|
+
sys.stdout.flush()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def preserve_terminal_mode() -> Generator[None]:
|
|
47
|
+
"""
|
|
48
|
+
Restore the terminal's mode when this context is left, e.g. around code that may
|
|
49
|
+
change it. In the meantime, the terminal is in raw mode, as it is while keys are read
|
|
50
|
+
(see ``KeyListener``). Does nothing if stdin isn't a terminal.
|
|
51
|
+
"""
|
|
52
|
+
if sys.stdin is None or not sys.stdin.isatty():
|
|
53
|
+
yield
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
with create_input().raw_mode():
|
|
57
|
+
yield
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-interactive-courses
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: PyICs (Python Interactive Courses) is a modular framework for teaching programming in Python locally, allowing for co-learning of a programming environment.
|
|
5
|
+
Author: johannes-langer
|
|
6
|
+
Author-email: johannes-langer <johannes.langer@uni-bamberg.de>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: colorama>=0.4.6 ; sys_platform == 'win32'
|
|
10
|
+
Requires-Dist: ipykernel>=7.3.0
|
|
11
|
+
Requires-Dist: json-tricks>=3.17.3
|
|
12
|
+
Requires-Dist: jsonargparse>=4.48.0
|
|
13
|
+
Requires-Dist: packaging>=24
|
|
14
|
+
Requires-Dist: platformdirs>=4
|
|
15
|
+
Requires-Dist: pluggy>=1.5
|
|
16
|
+
Requires-Dist: prompt-toolkit>=3.0.53
|
|
17
|
+
Requires-Dist: pyfiglet>=1.0.4
|
|
18
|
+
Requires-Dist: pytest>=9.0.3
|
|
19
|
+
Requires-Dist: rich>=15.0.0
|
|
20
|
+
Requires-Dist: ruff>=0.15.12
|
|
21
|
+
Requires-Dist: snoop>=0.6.0
|
|
22
|
+
Requires-Python: >=3.14
|
|
23
|
+
Project-URL: Repository, https://github.com/johannes-langer/PyICs
|
|
24
|
+
Project-URL: Issues, https://github.com/johannes-langer/PyICs/issues
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# PyICs
|
|
28
|
+
|
|
29
|
+
_Python Interactive Courses_
|
|
30
|
+
|
|
31
|
+
## What is PyICs?
|
|
32
|
+
|
|
33
|
+
PyICs is a framework for interactive Python tutorials, inspired by the _rustlings_ tutorial for the _Rust_ programming language. It strives to strike a balance between ease of use and feature volume, for both instructors and students.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
PyICs is available via ``pip``. Install the latest version using ``pip install python-interactive-courses``, requiring ``python>=3.14``. PyICs is available on Windows (Windows 10 or later), MacOS, and Linux.
|
|
38
|
+
|
|
39
|
+
## Using the PyICs App
|
|
40
|
+
|
|
41
|
+
### As a Student
|
|
42
|
+
|
|
43
|
+
#### Accessing Courses
|
|
44
|
+
|
|
45
|
+
Courses come from your instructor: either as files (a course manifest, ``*.pyicscourse.json``, and its task files) or as a course archive (a ``.zip`` file), or from a course library (a _source_) that you add once by the URL of its index, which your instructor gives you:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pyics [options] add_source [--name NAME] https://example.org/pyics-courses/index.json
|
|
49
|
+
pyics search # the courses of your sources; pyics search graphs finds matching ones
|
|
50
|
+
pyics pull search-algorithms # pulls it into ./search-algorithms
|
|
51
|
+
pyics # start it from the course selector
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- ``pyics pull`` can also take the URL or path of a course archive, and ``<source>/<course>`` if everal of your sources have a course of that name. ``--dest <directory>`` pulls it elsewhere.
|
|
55
|
+
- ``pyics update`` (in the course's directory, or ``pyics update <directory>``) updates a pulled course to its latest version. Your exercise files are kept as they are, and so is your progress.
|
|
56
|
+
- ``pyics sources`` lists your sources, and ``pyics remove_source <name>`` removes one. Sources are stored in ``sources.json`` in your pyics configuration directory (e.g. ``~/.config/pyics`` on Linux, ``~/Library/Application Support/pyics`` on macOS, ``%APPDATA%\pyics`` on Windows), so they stay when pyics is upgraded. Administrators can add sources for all users of a computer in the site configuration directory (e.g. ``/etc/xdg/pyics/sources.json``). The ``PYICS_CONFIG_DIR`` environment variable sets another configuration directory, whose sources are then the only ones used.
|
|
57
|
+
- The course selector also offers the courses of your sources that you haven't pulled yet, as they were when you last added, searched or pulled from them. Selecting one pulls it into the working directory.
|
|
58
|
+
|
|
59
|
+
> Courses run code on your computer, as their tests run your code together with theirs: only add sources and pull courses from people you trust. Sources are fetched over ``https`` (or from local files, e.g. on a network share); plain ``http`` has to be allowed with ``--allow_http``.
|
|
60
|
+
|
|
61
|
+
Some courses need packages besides ``pyics``, e.g. ``numpy``. They have to be installed in the same environment where PyICs is installed, siunce the tests run in the same Python interpreter. ``pyics search`` and the course selector show which of them are missing, and ``pyics pull`` prints the command that installs them (or runs it with ``--install_deps``, after asking). Packages are never installed without asking.
|
|
62
|
+
|
|
63
|
+
#### Navigating Courses
|
|
64
|
+
|
|
65
|
+
The main app starts with a course selector, listing the course manifests found in the working directory and up to two directories below it (``.venv``, ``.git`` and the like are skipped). Once you have selected a course, <kbd>c</kbd> goes back to the course selector, and <kbd>esc</kbd> from there returns to the course that was left. Courses left this way resume where they were left.
|
|
66
|
+
|
|
67
|
+
When a course is opened, it opens to the first unsolved task in the course. The main body of the app shows a number of tests which fail for the exercise file(s) for the current task, and a link to the file(s) at the very top.
|
|
68
|
+
Once all tests configured for a task pass, the task counts as completed. When that is the case <kbd>n</kbd> progresses to the next task, and <kbd>p</kbd> returns to the previous one.
|
|
69
|
+
|
|
70
|
+
##### Help Display
|
|
71
|
+
|
|
72
|
+
Tasks can have hints (``help`` in the task file), which ``create_task`` and ``edit_task`` ask for after the instructions. In the main app, <kbd>h</kbd> reveals the current task's next hint above its test results, one at a time and in order. Revealed hints stay revealed until the app is quit, also when you go to another task and back.
|
|
73
|
+
|
|
74
|
+
##### Tracing Failed Tests
|
|
75
|
+
|
|
76
|
+
When a test fails, <kbd>t</kbd> runs the task's tests again with [snoop](https://github.com/alexmojaki/snoop), which shows below each failed test what your code did: every line that was run, the values of the variables it changed, and the calls it made, with their arguments and return values. This trace mode stays on while you keep saving your exercise file, until <kbd>t</kbd> is pressed again, and it is off again for every other task you move to.
|
|
77
|
+
|
|
78
|
+
Function tests (``SimpleTest``) trace the call of the tested function, and output tests (``PrintTest``) the run of the whole file. Their authors can turn tracing off for a test by creating it with ``traceable=False``, e.g. so that its arguments aren't revealed. Tests that check the structure or the signatures of your code (``StructureTest``, ``SpecTest``) aren't traced.
|
|
79
|
+
|
|
80
|
+
#### Jupyter Notebooks
|
|
81
|
+
|
|
82
|
+
- A notebook comes without a kernel, so Jupyter or VS Code asks you to select one. Choose the environment in which PyICs is installed, as that is where the tests run.
|
|
83
|
+
- The tests of a task run whenever you **save** its notebook (<kbd>ctrl</kbd>+<kbd>s</kbd>, or when Jupyter autosaves it), like when you save a Python file.
|
|
84
|
+
- They run the code cells of the saved notebook from the top, the way _Restart Kernel and Run All Cells_ does. An error in a cell doesn't stop the run, so an unfinished exercise further down doesn't fail the tests of the exercises above it. When a test fails, its message names the cells that raised an error, e.g. ``cell 7 raised NameError: name 'qeue' is not defined``. Cells are counted from the top, markdown cells included.
|
|
85
|
+
- Notebook-only syntax in a test run:
|
|
86
|
+
- ``%%time``, ``%%timeit`` and ``%%capture`` run their cell once, without timing it, and so do ``%time`` and ``%timeit`` for their line. What ``%%capture`` captures isn't part of the output.
|
|
87
|
+
- Other cell magics (e.g. ``%%writefile``, ``%%bash``) skip their cell, and line magics (e.g. ``%matplotlib inline``, ``%pip install``) and shell commands (``!ls``) do nothing: ``files = !ls`` gives an empty list.
|
|
88
|
+
- ``display(x)`` prints ``x`` as Jupyter shows it as text (its ``repr``), whether or not you import it from ``IPython.display``. Plots are drawn without a window, so ``plt.show()`` doesn't wait for one to be closed.
|
|
89
|
+
- Cells tagged ``skip-execution`` don't run in tests, e.g. a long download or training run. Tags are edited in JupyterLab's property inspector, or with _Add Cell Tag_ in VS Code.
|
|
90
|
+
- Output tests (``PrintTest``) compare what all cells output: what they print, and what Jupyter shows as text, i.e. displayed objects, and the value of a cell whose last line is an expression (``Out[n]``), unless it is ``None`` or the line ends with ``;``. The expected output only has to be part of it, so what you output besides, e.g. while trying things out, doesn't fail the test.
|
|
91
|
+
- Traces (<kbd>t</kbd>) and the messages of structure tests name cells and their lines, e.g. ``bfs.ipynb:cell 3`` or ``cell 3, line 2``.
|
|
92
|
+
|
|
93
|
+
### As an Instructor
|
|
94
|
+
|
|
95
|
+
PyICs packages a number of scripts to create, edit, and distribute courses. The process of creating a course and recommended practices are described below.
|
|
96
|
+
|
|
97
|
+
#### Courses and Tasks
|
|
98
|
+
|
|
99
|
+
In its background PyICs distinguishes courses and tasks. A course packages multiple tasks, and is detected by PyICs in the current working directory (recursively, up to 3 layers deep) via its file extension (``.pyicscourse.json``). A course does **not** store any information on its tasks other than paths to the course files. This will most likely be addressed in a future version.
|
|
100
|
+
|
|
101
|
+
Tasks on the other hand package information on a single task, including the paths and contents of exercise and solution files, as well as serialized versions of the configured dynamic tests (see below).
|
|
102
|
+
|
|
103
|
+
You can create a _course_ by running the following command:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
pyics create_course [--fp file_path]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The script will ask you to either add tasks you have already created, or directly launch the task creation script which is also available as a standalone using the following command:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
pyics create_task [--fp file_path]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Note that while the task name is mostly for indexing purposes, the task instructions are displayed in the PyICs header while the task is active. Both task and course creation scripts run interactively, but the task creation script requires you to provide **at least one exercise file**. The script does not create the file for you; it expects a path to an existing file. Exercise files are created on the student's machine when a task is opened.
|
|
116
|
+
|
|
117
|
+
Similarly, solution files are provided as paths to existing files, but a task can be created without a solution file.
|
|
118
|
+
|
|
119
|
+
Finally, test creation is run interactively (see below for details on available tests). It is recommended to have a file ready which contains Python objects required for the tests, i.e. the parameters of the tests' constructors. If such a file is provided, the script allows you to select symbols for each of the parameters from those available in the provided file. This is particularly helpful for ``SpecTest``s, as the signatures can be inferred from class or function definitions in a file.
|
|
120
|
+
**Note:** If a symbol name matches a parameter name in the test's constructor, the symbol is automatically selected.
|
|
121
|
+
|
|
122
|
+
Existing tasks and courses can be edited using the following commands
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
pyics edit_task path-to-task-file
|
|
126
|
+
pyics edit_course path-to-course-file
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### Tests
|
|
130
|
+
|
|
131
|
+
The following tests can be configured. If a desired test can't be represented by the options below, please open an issue describing the desired behavior. Any test requires the following two parameters:
|
|
132
|
+
|
|
133
|
+
- ``name``: The name of the test, displayed in the test results if the test fails.
|
|
134
|
+
- ``file_path``: The path to the exercise file the test targets.
|
|
135
|
+
|
|
136
|
+
##### ``SimpleTest``
|
|
137
|
+
|
|
138
|
+
Simple tests are unit tests run on student's code. A ``SimpleTest`` additionally expects the following parameters:
|
|
139
|
+
|
|
140
|
+
- ``function_name``: The name of the function the test executes.
|
|
141
|
+
- ``expected``: The expected result, i.e. output of the function wrt...
|
|
142
|
+
- ``function_args``: Positional arguments to pass to the function in order to produce the expected output
|
|
143
|
+
- ``function_kwargs``: Keyword arguments to pass to the function in order to produce the expected output
|
|
144
|
+
- ``traceable``: Whether a detailed trace of the function can be displayed. Defaults to ``True``, can be turned off if the trace would give away too much of the solution (use sparingly).
|
|
145
|
+
|
|
146
|
+
##### ``SpecTest``
|
|
147
|
+
|
|
148
|
+
Spec tests are signature checks on the student's code. A ``SpecTest`` additionally expects the following parameters:
|
|
149
|
+
|
|
150
|
+
- ``symbol_name``: The name of the symbol where the expected signature needs to be located.
|
|
151
|
+
- ``expected``: The expected signature. Pass a Type (class) or function/method here. It will be converted to a spec_dict, which can then be serialized and checked against.
|
|
152
|
+
- ``by_type``: If this flag is set to true, check the signature based on type annotations instead of parameter/attribute names. This excludes the ``symbol_name``, which still needs to match.
|
|
153
|
+
|
|
154
|
+
##### ``StructureTest``
|
|
155
|
+
|
|
156
|
+
Structure tests can test _how_ tasks are solved by leveraging the ``ast`` builtin library. A ``StructureTest`` additionally expects the following parameter:
|
|
157
|
+
|
|
158
|
+
- ``rules``: A list of rules enforced by this test. A detailed documentation on how these rules can be configured is found in the [``StructureTest`` documentation](./structure_test.md).
|
|
159
|
+
|
|
160
|
+
##### ``PrintTest``
|
|
161
|
+
|
|
162
|
+
Print tests can test whether the stdout of students' files match a desired output. A ``PrintTest`` additionally expects the following parameters:
|
|
163
|
+
|
|
164
|
+
- ``expected``: The expected output of the targeted file to stdout. The output of a Python file has to match it, while the output of a notebook only has to contain it (see [Jupyter Notebooks](#jupyter-notebooks)).
|
|
165
|
+
- ``inputs``: A list of inputs to provide to the file in case the running script expects inputs (from the builtin ``input()`` function).
|
|
166
|
+
- ``traceable``: Whether a detailed trace of teh function can be displayed. Defaults to ``True``, can be turned off if the trace would give away too much of the solution (use sparingly).
|
|
167
|
+
|
|
168
|
+
#### Distribution
|
|
169
|
+
|
|
170
|
+
A course is distributed as an archive holding the course manifest and its task files, but not the exercise or solution files, which are created from the data stored in the task files. Progress is not tracked via the course files, but rather via the exercises that are already solved (exercise files are not overwritten if they already exist). The following command automatically creates a course archive:
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
pyics [options] pack [--out OUT] [--version VERSION] [--description DESCRIPTION] [--slug SLUG] [--tags TAGS [TAGS ...]] [--include INCLUDE [INCLUDE ...]] [--no_solutions] [--yes] manifest
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The ``--include`` option can be used to add other files the course needs (e.g. datasets), and ``--no_solutions`` removes the solution files from the tasks (if they existed). ``--version``, ``--description``, and ``--tags`` are stored in the manifest, so if a course is ever re-packed it will re-use the given values. ``--slug`` is the name a course is pulled by, and it is derived from the course's name unless an explicit name is provided via this option. ``--yes`` automatically accepts confirmation prompts.
|
|
177
|
+
``pack`` also finds the packages the course uses besides ``pyics`` and the standard library (imported by its exercise and solution code, or needed for the values its tests store, like numpy arrays), and offers to declare those that aren't declared in the manifest's ``requires`` yet. A task file may only make PyICs import modules of the packages its course declares: any other module is refused before the task is loaded.
|
|
178
|
+
|
|
179
|
+
``build`` indes lists the newest version of each course in the archives below the directory in ``index.json``.
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
pyics [options] build_index [--out OUT] [--name NAME] [directory]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Students add its URL with ``pyics add_source``. A course archive can also be pulled directly by its URL or path. An index looks like this, with the archives' URLs relative to it.
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"pyics_index": 1,
|
|
190
|
+
"name": "Uni Bamberg AI courses",
|
|
191
|
+
"courses": {
|
|
192
|
+
"search-algorithms": {
|
|
193
|
+
"title": "Search Algorithms",
|
|
194
|
+
"description": "DFS and BFS",
|
|
195
|
+
"version": "1.0.0",
|
|
196
|
+
"requires_pyics": ">=0.0.1",
|
|
197
|
+
"requires": ["numpy>=1.26"],
|
|
198
|
+
"url": "search-algorithms-1.0.0.zip",
|
|
199
|
+
"sha256": "...",
|
|
200
|
+
"tags": ["graphs"]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Archives are checked before anything of them is used: they can't write files outsinde the directory they are pulled to, their has h has to match the index, and they can only name the modules of the packages they declare.
|
|
207
|
+
|
|
208
|
+
#### Jupyter Notebooks
|
|
209
|
+
|
|
210
|
+
- ``create_task`` and ``edit_task`` accept notebooks as exercise and solution files, and all tests work with them. ``SimpleTest``s and ``SpecTest``s find functions and classes by name, in whichever cell students define them. ``StructureTest``s check the code of all code cells: cells that can't be parsed are left out, and named in the message if the test fails. ``PrintTest``s compare the output of all cells. Both can be restricted to a part of the notebook instead (see [Sections](#sections)).
|
|
211
|
+
- The output of a notebook only has to **contain** the ``expected`` output of a ``PrintTest``, rather than match it like the output of a Python file, since students also output other things in the cells of an exercise, e.g. the value of an expression they left at the end of a cell. Whitespace at the ends of lines is ignored, as usual. Short expected outputs are found easily: ``[a, b]`` is also found in ``[[a, b]]``, and ``3`` in ``13``, so expect enough to tell a right output from a wrong one, e.g. ``Sum: 3``. A test that expects no output still fails if there is any.
|
|
212
|
+
- Notebooks are stored normalized in task files: without their outputs and execution counts, so that the outputs of solutions don't reach students and task files stay small. Their kernel (``kernelspec``) isn't stored either, as it names an environment on your machine: students are asked to select a kernel of their own when they first open or run the notebook. A notebook you merely ran, or ran with another kernel, doesn't count as changed when you edit the task. If it did change, ``edit_task`` shows the difference as the text of its cells (``# %% cell 3`` blocks) rather than as JSON. If only its metadata changed (e.g. the tags of its cells), it says so.
|
|
213
|
+
- A notebook, e.g. the solution notebook, can be the test information file. Its code cells run as they do in tests, and its variables are offered for the test's parameters, e.g. a function as the ``expected`` signature of a ``SpecTest``. An error in a cell doesn't stop the run, and the prompts name the cell that raised it.
|
|
214
|
+
- The tests run in the directory PyICs was started in, while Jupyter runs a notebook in the notebook's directory: a file that a notebook opens by a relative path is looked up in different directories by the two.
|
|
215
|
+
- Keep the exercise notebooks in a visible directory, as ``examples/search_notebook`` does, rather than in ``.exercises/``: JupyterLab only opens notebooks in hidden directories if it runs with ``--ContentsManager.allow_hidden=True``.
|
|
216
|
+
- ``pack`` finds the packages imported in the code cells of notebooks, too. If the course's ``requires`` don't declare them, it shows the ``%pip install`` (and ``!pip install``) lines of its notebooks as hints, e.g. ``lecture.ipynb (cell 2): %pip install numpy``. These lines do nothing in tests, so the packages have to be declared. ``pack`` also warns about tasks that share a notebook, but store different contents for it (see [Sections](#sections)).
|
|
217
|
+
- An alternative that needs no notebook support is to pair a notebook with a Python file using [Jupytext](https://jupytext.readthedocs.io), and to use the ``.py`` file as the exercise file (magics are commented out in it). Students then need Jupytext in their Jupyter, and pairing tends to confuse beginners.
|
|
218
|
+
|
|
219
|
+
Sections link a task to a part of a notebook, e.g. to one exercise of a lecture's notebook that holds several.
|
|
220
|
+
|
|
221
|
+
- **Where a section starts:** at a cell tagged ``pyics:<name>``, or at a cell whose first line is the marker ``<!-- pyics: <name> -->`` (a markdown cell) or ``# pyics: <name>`` (a code cell). Names consist of letters, digits, ``_``, ``.`` and ``-``, e.g. ``ex2``. Tags are Jupyter's own way to mark cells, but hidden in most frontends. Markers are visible everywhere, and students can restore one they deleted.
|
|
222
|
+
- **Where it ends:** just before the next section starts, or at the end of the notebook. Cells above the first section belong to none. If several cells start a section of the same name (e.g. a copied cell), the first of them does.
|
|
223
|
+
- **Where to put it:** on the exercise's description, a markdown cell that students have no reason to change. The cells they add below it, split or move belong to the section, as long as they stay above the next one.
|
|
224
|
+
- **A task's section:** ``create_task`` and ``edit_task`` ask which section of each notebook exercise file the task is about (if the notebook has sections), and store it in the task file (``"sections"``, by the notebook's path relative to the task file). Older versions of PyICs ignore it.
|
|
225
|
+
- **Its tests:** ``StructureTest``s check only the code cells of the section, and fail if one of them can't be parsed. ``PrintTest``s compare only the output of the section's cells, while the whole notebook still runs, as a section usually needs the imports and data above it. ``SimpleTest``s and ``SpecTest``s find functions and classes by name, wherever they are defined, and aren't affected by sections.
|
|
226
|
+
- **A test's own section:** ``StructureTest``s and ``PrintTest``s use the task's section, unless they are given one of their own: ``section="ex1"`` for another section, or ``section=None`` for the whole notebook. ``create_task`` offers _The task's section_, _Whole notebook_ and the notebook's other sections. A test that uses the task's section stores nothing, so it follows when the task's section is changed.
|
|
227
|
+
- **Finding it:** the title of the main panel names the task's section after the notebook's path, e.g. ``lecture.ipynb › Exercise 2: Breadth-First Search``: the first heading of the cell that starts it, or else its name, looked up in the saved notebook. JupyterLab's _Table of Contents_ and VS Code's _Outline_ (<kbd>ctrl</kbd>+<kbd>shift</kbd>+<kbd>o</kbd>) list the notebook's headings.
|
|
228
|
+
- **A missing section fails its tests** with an explanation, e.g. ``Section 'ex2' not found: the cell that starts it (tagged 'pyics:ex2', or starting with the marker '# pyics: ex2') was removed or its tag changed.``, and the title shows ``section 'ex2' not found``. Tests never check another part of the notebook instead.
|
|
229
|
+
- **Several tasks in one notebook:** each task lists the same notebook among its exercise files, with its own section, and is solved on its own. The first of them that is set up creates the notebook (an existing exercise file is never overwritten), so they should all store the same contents for it: ``pack`` warns if they don't, and so does ``edit_task`` after writing a task (choose _Update_ for the notebook to store it as it is on disk).
|
|
230
|
+
|
|
231
|
+
## Technical Information
|
|
232
|
+
|
|
233
|
+
This project is managed using ``uv``. If you want to contribute to the project, run ``uv sync`` after cloning the repository at the top level to create a virtual environment with the required packages installed.
|
|
234
|
+
|
|
235
|
+
### Repository/App Structure
|
|
236
|
+
|
|
237
|
+
The ``src/pyics`` directory contains the following submodules:
|
|
238
|
+
|
|
239
|
+
- ``data`` which models ``Task``s and ``Course``s
|
|
240
|
+
- ``scripts`` which contains the executable scripts
|
|
241
|
+
- ``tests`` which contains anything related to dynamic test objects (the core testing logic for student code)
|
|
242
|
+
- ``distribution`` which packs, publishes and pulls courses, and checks the packages they need
|
|
243
|
+
- ``notebook`` which reads Jupyter notebooks, runs their code cells as exercise files, and finds their sections
|
|
244
|
+
- ``plugins`` which loads plugins, packages that extend pyics through [pluggy](https://pluggy.readthedocs.io) hooks, e.g. ``pyics-jupyter``
|
|
245
|
+
- ``util`` containing logic for json handling and logging
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
pyics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
pyics/app/actions.py,sha256=wig2BYXJLL6FTT3c0YuDMGEHyS3Hxr_Gx-uXOymMaFo,4747
|
|
3
|
+
pyics/app/cli.py,sha256=9LBf5ssA99MUjHwLzi_kN_cDJK_gkqsR94nHOcm4EZ4,12620
|
|
4
|
+
pyics/app/key_listener.py,sha256=rXiZbehuyukCfBO2dyftyZaXFTVbyOgwr6eNmvVkuY0,4726
|
|
5
|
+
pyics/app/prompts.py,sha256=Vmr7z0_o2EXu46mNf3iLhkcfLx0bogemdE6oAAeiCbc,16362
|
|
6
|
+
pyics/app/pyics.py,sha256=AzGAbzxMcXds_WBTW-0zisk_odbpsyzHe_7-jrYDvW0,17528
|
|
7
|
+
pyics/data/__init__.py,sha256=TB-H_exM3p3MCF5lR-FYZ3f9pJmHOr-LN1xZGCWdyn0,76
|
|
8
|
+
pyics/data/course.py,sha256=xipAnrwUp8hx3sVhvahVsve9jHF3Ehvz4iesvN9EAd4,19875
|
|
9
|
+
pyics/data/task.py,sha256=SnC2O9qSzzNXlsnmFzhVXXk3DgU0tcsdIFdXor5toeY,28440
|
|
10
|
+
pyics/distribution/__init__.py,sha256=wDCDT-tkS62EsLTJwnGzjbiKsYm72ur1WgV1a4sQnT0,226
|
|
11
|
+
pyics/distribution/archive.py,sha256=QsOp9LcdKxQgRYW5M3XAlQEg931Ls6NBmcFTRT8KYf4,25704
|
|
12
|
+
pyics/distribution/dependencies.py,sha256=Lteez1HRNaZOwUwmd1XFYoOmJ6bukFBwB9d9f4pw9DY,17177
|
|
13
|
+
pyics/distribution/fetch.py,sha256=TkR6yk22IQXG6jOTLp72ERWDuLjWRnr7UYq3zHlIDIk,4753
|
|
14
|
+
pyics/distribution/pull.py,sha256=-oPh5gN_xXS53igt6aTAMLiB3OlfAVi4YkDEmnNSAQk,13767
|
|
15
|
+
pyics/distribution/slugs.py,sha256=QCtaOsra79v3HH_mN-zQLDUxXPAxc-cg4pud1xme3cI,778
|
|
16
|
+
pyics/distribution/sources.py,sha256=PiTng8-0sE168xij2PGNuEy40ZK2GGx_0FAufMMBAIk,19822
|
|
17
|
+
pyics/notebook/__init__.py,sha256=nKZG7OesmxBWWR10XD_0EXz0YfgaO37tofGeA6z5j7I,81
|
|
18
|
+
pyics/notebook/cells.py,sha256=eu5FLfPWPfboDD5xqmakgtd-r-p-SE1AOmB4FFi2vzk,6556
|
|
19
|
+
pyics/notebook/loader.py,sha256=hVB9eZbTwBOBqECESwgxnpq9wINlzAgxpACSGw6KxF4,22742
|
|
20
|
+
pyics/notebook/sections.py,sha256=IIWTqJee4pM4pMqi46FgR2uzD4U0FBAntF6QDuEsVKw,7329
|
|
21
|
+
pyics/plugins.py,sha256=PkQMX9l0P6UAwsgBfk6-xAbZBQ9QKmz_NwN1ob9FnvA,3138
|
|
22
|
+
pyics/scripts/console.py,sha256=ZKEVX4oE6p9EpHJtEnkyHzxaxCKFnMYi46q9SS6_QMQ,3003
|
|
23
|
+
pyics/scripts/create_course.py,sha256=dTEzlxHqbaXKgwglKVWK2WFyUgUlCe7fKrWuJ2EX_dw,11140
|
|
24
|
+
pyics/scripts/create_task.py,sha256=ywRboKNt6kaIIb2Mj7pyxgtk8n3GlNY5tbLwaIbeeeQ,32927
|
|
25
|
+
pyics/scripts/edit_course.py,sha256=xR_b-E_SGySsLya2UaEw33J8IFESujx46dTYgmN7-ao,5446
|
|
26
|
+
pyics/scripts/edit_task.py,sha256=6EcxQSMC4uogjNvkiDOo4ENB5-VYm_pRFU8tg-hsA3E,4299
|
|
27
|
+
pyics/scripts/main.py,sha256=5bYOmWcgc1JTokzU1JL8Ex-Ff1iuqhbubYuyynx2Wjs,4516
|
|
28
|
+
pyics/scripts/pack.py,sha256=8E_XAGuY8vnsomOHeLttE2PiCQL4k1NoJnyMIGBIMVs,7916
|
|
29
|
+
pyics/scripts/pull.py,sha256=mUS_hz2vBTULjdZFG5dkKc9xk22sYwlnHPo65YqkxxQ,4420
|
|
30
|
+
pyics/scripts/sources.py,sha256=jGeJDH32nebVnAcEjmlaveFa4wY-yb0vD_1ueNmiGTw,4164
|
|
31
|
+
pyics/tests/__init__.py,sha256=1CMagUMIq3lkC5q2Qq2TaQPrT6X3HcVXKYHXuD6U3Oo,166
|
|
32
|
+
pyics/tests/capture.py,sha256=8OKVK5U4HSqRB7Nj6tiXkuamUDmRE-IeOBjIGZrdS4M,3281
|
|
33
|
+
pyics/tests/check_specs.py,sha256=x3KxDQ1KgktnJmRnNpKbIcdJ7wEVAgdfgETBEylpC6c,19385
|
|
34
|
+
pyics/tests/check_structure.py,sha256=QXrAWhbznx0ZQIKHcDMWXFmWSLiu89mWU2gAJOwsdls,26875
|
|
35
|
+
pyics/tests/compare.py,sha256=WatNKrho4eyoIM8CvANqAdT8jy3ZgDcEoUMqIR6i6N4,7109
|
|
36
|
+
pyics/tests/pytest_integration.py,sha256=vgTrQnK9RqZ3LudDTmLdAD4fm9BpFmE1LhuQ1uTi2cU,8276
|
|
37
|
+
pyics/tests/results.py,sha256=2A23iqIV_6kN5pCzCbcjCOUvnP8jawEvBe6Y87mxJpc,5240
|
|
38
|
+
pyics/tests/tests.py,sha256=b844VKGzzvZsvul-6a5pUHLvus7CxPP6dMadnWlnZpY,17126
|
|
39
|
+
pyics/tests/trace.py,sha256=zRgKClJXUxDf0p36XN0szbYOKRAPj3ooeecSdox60Z4,5545
|
|
40
|
+
pyics/util/discovery.py,sha256=g3jtWhn2BHny4T0v7h1x0RoKN9jzu38str3rjpz407s,3084
|
|
41
|
+
pyics/util/json_handling.py,sha256=HX_xp9v6PtQc60Y7YN0yOkiIp45SJNZovkFo2fTx9FY,6361
|
|
42
|
+
pyics/util/logging.py,sha256=qQKAJZ2th5Q_2add7XN-zCzRkvus_wgKHVvt3-eMN2E,854
|
|
43
|
+
pyics/util/paths.py,sha256=RH__GAywFPQf_E1mrQGA6JzrqHQvIgGKsPiKGmxxoHc,1305
|
|
44
|
+
pyics/util/terminal.py,sha256=nyTvNC4wxKYxtBd9URxIz65xNeVMluNbnct9bKVLYNI,1628
|
|
45
|
+
python_interactive_courses-1.0.0.dist-info/licenses/LICENSE,sha256=PUrGQXGE5VsEi2wCS6xiW9J1N75eJtSkg12DWpiy-n4,1072
|
|
46
|
+
python_interactive_courses-1.0.0.dist-info/WHEEL,sha256=xTCBFIZIIyRSxyowOv5Wqg_jFU6KsdIffuulBB09Ph8,80
|
|
47
|
+
python_interactive_courses-1.0.0.dist-info/entry_points.txt,sha256=YDzxN7dg2P2fh9rAfv11R-9cZqVeUv51odlXv68lgdo,50
|
|
48
|
+
python_interactive_courses-1.0.0.dist-info/METADATA,sha256=uhmvi23b_sKvkEhHx4fSi4RUHb67Njnuq-nLnjkp-KA,23506
|
|
49
|
+
python_interactive_courses-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johannes Langer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|