markten 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.
- markten-0.1.0/LICENSE +21 -0
- markten-0.1.0/PKG-INFO +84 -0
- markten-0.1.0/README.md +69 -0
- markten-0.1.0/markten/__consts.py +5 -0
- markten-0.1.0/markten/__init__.py +15 -0
- markten-0.1.0/markten/__main__.py +34 -0
- markten-0.1.0/markten/__permutations.py +38 -0
- markten-0.1.0/markten/__recipe.py +261 -0
- markten-0.1.0/markten/__spinners.py +236 -0
- markten-0.1.0/markten/__term_tools.py +97 -0
- markten-0.1.0/markten/__utils.py +27 -0
- markten-0.1.0/markten/actions/__action.py +47 -0
- markten-0.1.0/markten/actions/__async_process.py +52 -0
- markten-0.1.0/markten/actions/__init__.py +21 -0
- markten-0.1.0/markten/actions/editor.py +27 -0
- markten-0.1.0/markten/actions/git.py +67 -0
- markten-0.1.0/markten/actions/process.py +89 -0
- markten-0.1.0/markten/actions/python.py +44 -0
- markten-0.1.0/markten/actions/time.py +44 -0
- markten-0.1.0/markten/more_itertools.py +45 -0
- markten-0.1.0/markten/parameters/__fs.py +63 -0
- markten-0.1.0/markten/parameters/__init__.py +15 -0
- markten-0.1.0/markten/parameters/__io.py +18 -0
- markten-0.1.0/markten/parameters/__object.py +23 -0
- markten-0.1.0/pyproject.toml +19 -0
markten-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 COMP1010 UNSW
|
|
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.
|
markten-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: markten
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A framework for automating the process of manual marking in bulk
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Maddy Guthridge
|
|
7
|
+
Author-email: hello@maddyguthridge.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# MarkTen
|
|
16
|
+
|
|
17
|
+
Assess your students' work with all of the delight and none of the tedium.
|
|
18
|
+
|
|
19
|
+
## Installing
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ pip install markten
|
|
23
|
+
...
|
|
24
|
+
Successfully installed markten-0.1.0
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or to install in an independent environment, you can use `pipx`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
$ pipx install markten
|
|
31
|
+
installed package markten 0.1.0, installed using Python 3.12.6
|
|
32
|
+
These apps are now globally available
|
|
33
|
+
- markten
|
|
34
|
+
done! ✨ 🌟 ✨
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
Define your recipe parameters. For example, this recipe takes in git repo names
|
|
40
|
+
from stdin.
|
|
41
|
+
|
|
42
|
+
```py
|
|
43
|
+
from markten import Recipe, parameters, actions
|
|
44
|
+
|
|
45
|
+
marker = Recipe("COMP2511 Lab Marking")
|
|
46
|
+
|
|
47
|
+
marker.parameter("repo", parameters.stdin("Repo name"))
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Write simple marking recipes by defining simple functions for each step.
|
|
51
|
+
|
|
52
|
+
```py
|
|
53
|
+
# Functions can take arbitrary parameters
|
|
54
|
+
def setup(repo: str):
|
|
55
|
+
"""Set up marking environment"""
|
|
56
|
+
# Clone the given git repo to a temporary directory
|
|
57
|
+
directory = actions.git.clone(f"git@github.com:COMP1010UNSW/{repo}.git")
|
|
58
|
+
return {
|
|
59
|
+
"directory": directory,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
marker.step("Clone repo", setup)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The parameters returned by your previous steps can be used in later steps, just
|
|
66
|
+
by giving the function parameters the same name.
|
|
67
|
+
|
|
68
|
+
```py
|
|
69
|
+
def open_code(directory: Path):
|
|
70
|
+
"""Open the cloned git repo in VS Code"""
|
|
71
|
+
return actions.editor.vs_code(directory)
|
|
72
|
+
|
|
73
|
+
marker.step("View in VS Code", open_code)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then run the recipe. It'll run for every permutation of your parameters, making
|
|
77
|
+
it easy to mark in bulk.
|
|
78
|
+
|
|
79
|
+
```py
|
|
80
|
+
marker.run()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
For more examples, see the examples directory.
|
|
84
|
+
|
markten-0.1.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# MarkTen
|
|
2
|
+
|
|
3
|
+
Assess your students' work with all of the delight and none of the tedium.
|
|
4
|
+
|
|
5
|
+
## Installing
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
$ pip install markten
|
|
9
|
+
...
|
|
10
|
+
Successfully installed markten-0.1.0
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or to install in an independent environment, you can use `pipx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
$ pipx install markten
|
|
17
|
+
installed package markten 0.1.0, installed using Python 3.12.6
|
|
18
|
+
These apps are now globally available
|
|
19
|
+
- markten
|
|
20
|
+
done! ✨ 🌟 ✨
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
Define your recipe parameters. For example, this recipe takes in git repo names
|
|
26
|
+
from stdin.
|
|
27
|
+
|
|
28
|
+
```py
|
|
29
|
+
from markten import Recipe, parameters, actions
|
|
30
|
+
|
|
31
|
+
marker = Recipe("COMP2511 Lab Marking")
|
|
32
|
+
|
|
33
|
+
marker.parameter("repo", parameters.stdin("Repo name"))
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Write simple marking recipes by defining simple functions for each step.
|
|
37
|
+
|
|
38
|
+
```py
|
|
39
|
+
# Functions can take arbitrary parameters
|
|
40
|
+
def setup(repo: str):
|
|
41
|
+
"""Set up marking environment"""
|
|
42
|
+
# Clone the given git repo to a temporary directory
|
|
43
|
+
directory = actions.git.clone(f"git@github.com:COMP1010UNSW/{repo}.git")
|
|
44
|
+
return {
|
|
45
|
+
"directory": directory,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
marker.step("Clone repo", setup)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The parameters returned by your previous steps can be used in later steps, just
|
|
52
|
+
by giving the function parameters the same name.
|
|
53
|
+
|
|
54
|
+
```py
|
|
55
|
+
def open_code(directory: Path):
|
|
56
|
+
"""Open the cloned git repo in VS Code"""
|
|
57
|
+
return actions.editor.vs_code(directory)
|
|
58
|
+
|
|
59
|
+
marker.step("View in VS Code", open_code)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then run the recipe. It'll run for every permutation of your parameters, making
|
|
63
|
+
it easy to mark in bulk.
|
|
64
|
+
|
|
65
|
+
```py
|
|
66
|
+
marker.run()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For more examples, see the examples directory.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# MarkTen / Main
|
|
3
|
+
|
|
4
|
+
Programmatic entrypoint to MarkTen, allowing it to be run as a script.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
from . import __utils as utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def show_info():
|
|
12
|
+
utils.show_banner()
|
|
13
|
+
print("Usage:")
|
|
14
|
+
print(" markten <recipe-script> [arguments]")
|
|
15
|
+
print(" This will execute the given script in Markten's Python")
|
|
16
|
+
print(" environment.")
|
|
17
|
+
print("License: MIT")
|
|
18
|
+
print("Author: Maddy Guthridge")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main():
|
|
22
|
+
if len(sys.argv) == 1 or sys.argv[1] in ["-h", "--help"]:
|
|
23
|
+
show_info()
|
|
24
|
+
exit(1)
|
|
25
|
+
else:
|
|
26
|
+
# Attempt to execute the given file with any remaining arguments
|
|
27
|
+
recipe = sys.argv[1]
|
|
28
|
+
args = sys.argv[2:]
|
|
29
|
+
|
|
30
|
+
os.execv(sys.executable, ("python", recipe, *args))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == '__main__':
|
|
34
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable, Mapping, Generator
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def recursive_generator(
|
|
8
|
+
keys: list[str],
|
|
9
|
+
params_dict: Mapping[str, Iterable[Any]],
|
|
10
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
11
|
+
"""
|
|
12
|
+
Recursively iterate over the given keys, producing a dict of values.
|
|
13
|
+
"""
|
|
14
|
+
keys_head = keys[0]
|
|
15
|
+
# Base case: this is the last remaining key
|
|
16
|
+
if len(keys) == 1:
|
|
17
|
+
for value in params_dict[keys_head]:
|
|
18
|
+
yield {keys_head: value}
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
# Recursive case, other keys remain, and we need to iterate over those too
|
|
22
|
+
keys_tail = keys[1:]
|
|
23
|
+
|
|
24
|
+
for value in params_dict[keys_head]:
|
|
25
|
+
# Iterate over remaining keys
|
|
26
|
+
for current_params in recursive_generator(keys_tail, params_dict):
|
|
27
|
+
# Overall keys is the union of the current key-value pair with
|
|
28
|
+
# the params yielded by the recursion
|
|
29
|
+
yield {keys_head: value} | current_params
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def dict_permutations_iterator(
|
|
33
|
+
params: Mapping[str, Iterable[Any]],
|
|
34
|
+
) -> Generator[dict[str, Any], None, None]:
|
|
35
|
+
"""
|
|
36
|
+
Iterate over all possible parameter values provided by the generators.
|
|
37
|
+
"""
|
|
38
|
+
return recursive_generator(list(params.keys()), params)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
# MarkTen / Recipe
|
|
3
|
+
|
|
4
|
+
Contains the definition for the main MarkTen class.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import inspect
|
|
8
|
+
from traceback import print_exception
|
|
9
|
+
from .actions import MarkTenAction
|
|
10
|
+
from typing import Union, Callable, Any
|
|
11
|
+
from collections.abc import Mapping, Iterable
|
|
12
|
+
from .__permutations import dict_permutations_iterator
|
|
13
|
+
from . import __utils as utils
|
|
14
|
+
from .__spinners import SpinnerManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ParameterPermutations = Mapping[str, Iterable[Any]]
|
|
18
|
+
"""
|
|
19
|
+
Mapping containing iterables for all permutations of the available params.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
GeneratedActions = Union[
|
|
23
|
+
MarkTenAction,
|
|
24
|
+
tuple[MarkTenAction, ...],
|
|
25
|
+
Mapping[str, MarkTenAction],
|
|
26
|
+
]
|
|
27
|
+
"""
|
|
28
|
+
`GeneratedActions` is a collection of actions run in parallel as a part of a
|
|
29
|
+
step in the marking recipe.
|
|
30
|
+
|
|
31
|
+
This can be one of:
|
|
32
|
+
|
|
33
|
+
* `MarkTenAction`: a single anonymous action, whose result is discarded.
|
|
34
|
+
* `tuple[MarkTenAction, ...]`: a collection of anonymous actions.
|
|
35
|
+
* `Mapping[str, MarkTenAction]`: a collection of named actions, whose results
|
|
36
|
+
are stored as parameters under the given names.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
ActionGenerator = Callable[..., 'ActionStep']
|
|
40
|
+
"""
|
|
41
|
+
An `ActionGenerator` is a function that may accept any current parameters, and
|
|
42
|
+
must return an `ActionStep`, which is expanded recursively.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
ActionStepItem = Union[
|
|
47
|
+
ActionGenerator,
|
|
48
|
+
GeneratedActions,
|
|
49
|
+
]
|
|
50
|
+
"""
|
|
51
|
+
Each item in a step must either be a function that generates actions, or
|
|
52
|
+
pre-generated actions.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
ActionStep = Union[
|
|
57
|
+
ActionStepItem,
|
|
58
|
+
tuple[ActionStepItem, ...]
|
|
59
|
+
]
|
|
60
|
+
"""
|
|
61
|
+
An `ActionStep` is a collection of items that should be executed in parallel.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
GeneratedActionStep = tuple[
|
|
65
|
+
dict[str, MarkTenAction],
|
|
66
|
+
list[MarkTenAction]
|
|
67
|
+
]
|
|
68
|
+
"""
|
|
69
|
+
An `ActionStep` after running any action generators.
|
|
70
|
+
|
|
71
|
+
This is used internally when running the actions.
|
|
72
|
+
|
|
73
|
+
A tuple of:
|
|
74
|
+
|
|
75
|
+
* `dict[str, MarkTenAction]`: named actions
|
|
76
|
+
* `list[MarkTenAction]`: anonymous actions
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Recipe:
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
recipe_name: str,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.__name = recipe_name
|
|
86
|
+
self.__params: dict[str, Any] = {}
|
|
87
|
+
self.__steps: list[tuple[str, ActionStep]] = []
|
|
88
|
+
|
|
89
|
+
def parameter(self, name: str, values: Iterable[str]) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Add a single parameter to the recipe.
|
|
92
|
+
"""
|
|
93
|
+
self.__params[name] = values
|
|
94
|
+
|
|
95
|
+
def parameters(self, parameters: ParameterPermutations) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Add a collection of parameters for the recipe.
|
|
98
|
+
"""
|
|
99
|
+
self.__params |= dict(parameters)
|
|
100
|
+
|
|
101
|
+
def step(self, name: str, step: ActionStep) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Add a step to the recipe
|
|
104
|
+
"""
|
|
105
|
+
self.__steps.append((name, step))
|
|
106
|
+
|
|
107
|
+
def run(self):
|
|
108
|
+
"""
|
|
109
|
+
Run the marking recipe for each combination given by the generators.
|
|
110
|
+
"""
|
|
111
|
+
asyncio.run(self.__do_run())
|
|
112
|
+
|
|
113
|
+
async def __do_run(self):
|
|
114
|
+
"""Async implementation of running the marking recipe"""
|
|
115
|
+
utils.show_banner()
|
|
116
|
+
print(f"Running recipe '{self.__name}'")
|
|
117
|
+
for params in dict_permutations_iterator(self.__params):
|
|
118
|
+
# Begin marking with the given parameters
|
|
119
|
+
show_current_params(params)
|
|
120
|
+
try:
|
|
121
|
+
await self.__run_recipe(params)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print("\n\n")
|
|
124
|
+
print_exception(e)
|
|
125
|
+
print()
|
|
126
|
+
|
|
127
|
+
print("Recipe ran for all inputs")
|
|
128
|
+
|
|
129
|
+
async def __run_recipe(self, params: Mapping[str, Any]):
|
|
130
|
+
"""Execute the marking recipe using the given params"""
|
|
131
|
+
params = dict(params)
|
|
132
|
+
|
|
133
|
+
actions_by_step: list[GeneratedActionStep] = []
|
|
134
|
+
"""
|
|
135
|
+
Actions ordered by step, used to ensure that we can run any required
|
|
136
|
+
teardown at the end of the recipe.
|
|
137
|
+
"""
|
|
138
|
+
for i, (name, step) in enumerate(self.__steps):
|
|
139
|
+
# Convert the step into a list of actions to be run in parallel
|
|
140
|
+
actions_to_run = generate_actions_for_step(step, params)
|
|
141
|
+
actions_by_step.append(actions_to_run)
|
|
142
|
+
|
|
143
|
+
spinners = SpinnerManager(f"{i + 1}. {name}")
|
|
144
|
+
|
|
145
|
+
# Run all tasks
|
|
146
|
+
named_tasks: dict[str, asyncio.Task[Any]] = {}
|
|
147
|
+
anonymous_tasks: list[asyncio.Task[Any]] = []
|
|
148
|
+
# Named tasks
|
|
149
|
+
for key, action in actions_to_run[0].items():
|
|
150
|
+
named_tasks[key] = asyncio.create_task(
|
|
151
|
+
action.run(spinners.create_task(action.get_name())))
|
|
152
|
+
# Anonymous tasks
|
|
153
|
+
for action in actions_to_run[1]:
|
|
154
|
+
anonymous_tasks.append(asyncio.create_task(
|
|
155
|
+
action.run(spinners.create_task(action.get_name()))))
|
|
156
|
+
# Start drawing the spinners
|
|
157
|
+
spinner_task = asyncio.create_task(spinners.spin())
|
|
158
|
+
# Now wait for them all to resolve
|
|
159
|
+
results: dict[str, Any] = {}
|
|
160
|
+
task_errors: list[Exception] = []
|
|
161
|
+
for key, task in named_tasks.items():
|
|
162
|
+
try:
|
|
163
|
+
results[key] = await task
|
|
164
|
+
except Exception as e:
|
|
165
|
+
task_errors.append(e)
|
|
166
|
+
for task in anonymous_tasks:
|
|
167
|
+
try:
|
|
168
|
+
await task
|
|
169
|
+
except Exception as e:
|
|
170
|
+
task_errors.append(e)
|
|
171
|
+
|
|
172
|
+
# Cancel the spinner task
|
|
173
|
+
spinner_task.cancel()
|
|
174
|
+
|
|
175
|
+
if len(task_errors):
|
|
176
|
+
raise ExceptionGroup(
|
|
177
|
+
f"Task failed on step {i + 1}",
|
|
178
|
+
task_errors,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Now merge the results with the params
|
|
182
|
+
params |= results
|
|
183
|
+
|
|
184
|
+
# Now perform the teardown
|
|
185
|
+
for named_actions, anonymous_actions in reversed(actions_by_step):
|
|
186
|
+
for action in named_actions.values():
|
|
187
|
+
await action.cleanup()
|
|
188
|
+
for action in anonymous_actions:
|
|
189
|
+
await action.cleanup()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def show_current_params(params: Mapping[str, Any]):
|
|
193
|
+
"""
|
|
194
|
+
Displays the current params to the user.
|
|
195
|
+
"""
|
|
196
|
+
print()
|
|
197
|
+
print("Running recipe with given parameters:")
|
|
198
|
+
for param_name, param_value in params.items():
|
|
199
|
+
print(f" {param_name} = {param_value}")
|
|
200
|
+
print()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def generate_actions_for_step(
|
|
204
|
+
step: ActionStep,
|
|
205
|
+
params: Mapping[str, Any],
|
|
206
|
+
) -> GeneratedActionStep:
|
|
207
|
+
"""
|
|
208
|
+
Given a step, generate the actions
|
|
209
|
+
"""
|
|
210
|
+
if isinstance(step, tuple):
|
|
211
|
+
result: GeneratedActionStep = ({}, [])
|
|
212
|
+
for step_item in step:
|
|
213
|
+
# Use recursion so that we can simplify the handling of multiple
|
|
214
|
+
# steps
|
|
215
|
+
result = union_generated_action_step_items(
|
|
216
|
+
result,
|
|
217
|
+
generate_actions_for_step(step_item, params)
|
|
218
|
+
)
|
|
219
|
+
return result
|
|
220
|
+
elif isinstance(step, MarkTenAction):
|
|
221
|
+
# Single anonymous action
|
|
222
|
+
return ({}, [step])
|
|
223
|
+
elif isinstance(step, Mapping):
|
|
224
|
+
# Collection of named actions
|
|
225
|
+
return (dict(step), [])
|
|
226
|
+
else:
|
|
227
|
+
# step is an ActionGenerator function
|
|
228
|
+
action_fn_output = execute_action_function(step, params)
|
|
229
|
+
# Parse the result recursively
|
|
230
|
+
return generate_actions_for_step(action_fn_output, params)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def union_generated_action_step_items(
|
|
234
|
+
a: GeneratedActionStep,
|
|
235
|
+
b: GeneratedActionStep,
|
|
236
|
+
) -> GeneratedActionStep:
|
|
237
|
+
"""
|
|
238
|
+
Union a and b.
|
|
239
|
+
"""
|
|
240
|
+
named_actions = a[0] | b[0]
|
|
241
|
+
anonymous_actions = a[1] + b[1]
|
|
242
|
+
return named_actions, anonymous_actions
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def execute_action_function(
|
|
246
|
+
fn: ActionGenerator,
|
|
247
|
+
params: Mapping[str, Any],
|
|
248
|
+
) -> ActionStep:
|
|
249
|
+
"""
|
|
250
|
+
Execute an action generator function, ensuring only the desired parameters
|
|
251
|
+
are passed as kwargs.
|
|
252
|
+
"""
|
|
253
|
+
args = inspect.getfullargspec(fn)
|
|
254
|
+
kwargs_used = args[2] is not None
|
|
255
|
+
if kwargs_used:
|
|
256
|
+
return fn(**params)
|
|
257
|
+
else:
|
|
258
|
+
# Only pass the args used
|
|
259
|
+
named_args = args[0]
|
|
260
|
+
param_subset = {k: v for k, v in params.items() if k in named_args}
|
|
261
|
+
return fn(**param_subset)
|