Skip to content

Commit 880a974

Browse files
authored
1.0.0 (#38)
* feat: add ability for effects to use matched value * build: add python path to docker image * docs: update doc and add new examples --------- Signed-off-by: Emilio Reyes <[email protected]>
1 parent 1ca223b commit 880a974

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1213
-1199
lines changed

Dockerfile

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
ARG PYTHON_VERSION=3.9
1+
ARG PYTHON_VERSION=3.12
22
FROM python:${PYTHON_VERSION}-slim
33
ENV PYTHONDONTWRITEBYTECODE=1
44
ENV TERM=xterm-256color
5+
ENV PYTHONPATH=/code/examples/
56
WORKDIR /code
67
COPY . /code/
78
RUN pip install --upgrade pip && \
8-
pip install pybuilder namegenerator
9+
pip install pybuilder namegenerator faker
910
RUN pyb -X

README.md

+25-75
Original file line numberDiff line numberDiff line change
@@ -4,120 +4,70 @@
44
[![complexity](https://img.shields.io/badge/complexity-A-brightgreen)](https://radon.readthedocs.io/en/latest/api.html#module-radon.complexity)
55
[![vulnerabilities](https://img.shields.io/badge/vulnerabilities-None-brightgreen)](https://pypi.org/project/bandit/)
66
[![PyPI version](https://badge.fury.io/py/mpcurses.svg)](https://badge.fury.io/py/mpcurses)
7-
[![python](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-teal)](https://www.python.org/downloads/)
7+
[![python](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.12-teal)](https://www.python.org/downloads/)
88

9-
Mpcurses is an abstraction of the Python curses and multiprocessing libraries providing function execution and runtime visualization capabilities at scale. It contains a simple API to enable any Python function to be executed across one or more background processes and includes built-in directives to visualize the functions execution on a terminal screen.
9+
The mpcurses package facilitates seamless terminal screen updates from child processes within a multiprocessing worker pool - leveraging the curses library for terminal manipulation. The `MPcurses` class is a subclass of [MPmq](https://pypi.org/project/mpmq/); a multiprocessing message queue which enables inter-process communication (IPC) between child workers and a parent process through queuing and consumption of log messages. Mpcurses provides a lightweight abstraction for the curses terminal screen, representing it as a Python dictionary. It includes predefined directives for updating the screen, encompassing:
1010

11-
The mpcurses API allows for seamless integration since it does not require the target function to include additional context about curses or multiprocessing. The target function does need to implement logging since log messages are the primary means of inter-process communication between the background processes executing the function and the main process updating the curses screen on the terminal.
11+
- Numeric counter management
12+
- Match messages using regular expressions
13+
- Text value and color updates
14+
- Visual indicator maintenance
15+
- Progress bar rendering
16+
- Table and list displays
1217

13-
The main features are:
14-
15-
* Execute a function across one or more concurrent processes
16-
* Queue execution to ensure a predefined number of processes are running
17-
* Visualize function execution on the terminal screen using curses
18-
* Define the screen layout using a Python dict
19-
* Leverage built-in directives to dynamically update the screen when events occur by analyzing log messages
20-
* Keep numeric counts
21-
* Update text values and colors
22-
* Maintain visual indicators
23-
* Display progress bars
24-
* Display tables
25-
* Display lists
26-
27-
Refer to documentation here: https://soda480.github.io/mpcurses/
28-
29-
Mpcurses is a subclass of `mpmq`, see [mpmq](https://pypi.org/project/mpmq/) for more information.
18+
Refer to the MPcurses documentation here: https://soda480.github.io/mpcurses/
3019

3120
### Installation
3221
```bash
3322
pip install mpcurses
3423
```
35-
3624
### Examples
3725

38-
To run the samples below you need to install the namegenerator module `pip install namegenerator`
39-
40-
41-
A simple example using mpcurses:
26+
Invoke a single child process to execute a task defined by the `do_something` function. Mpcurses captures all log messages and sends them to a thread-safe queue, the main process consumes messages and uses regular expressions to update the screen which is represented as a dictionary.
4227

4328
```python
4429
from mpcurses import MPcurses
4530
import namegenerator, time, logging
4631
logger = logging.getLogger(__name__)
4732

4833
def do_something(*args):
49-
for _ in range(0, 600):
34+
for _ in range(0, 400):
5035
logger.debug(f'processing item "{namegenerator.gen()}"')
5136
time.sleep(.01)
5237

5338
MPcurses(
5439
function=do_something,
5540
screen_layout={
5641
'display_item': {
57-
'position': (1, 1),
58-
'text': 'Processing:',
59-
'text_color': 0,
60-
'color': 14,
61-
'clear': True,
62-
'regex': r'^processing item "(?P<value>.*)"$'
63-
}
42+
'position': (1, 1), 'text': 'Processing:', 'text_color': 0, 'color': 14,
43+
'clear': True, 'regex': r'^processing item "(?P<value>.*)"$'}
6444
}).execute()
6545
```
6646

6747
Executing the code above results in the following:
68-
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example.gif)
48+
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/demo.gif)
6949

70-
To scale execution of the function across multiple processes; we set the `process_data` parameter to a list whose total number of elements represent the number of processes to execute, and each list element represent the data to be passed to each respective process:
71-
72-
```python
73-
from mpcurses import MPcurses
74-
import namegenerator, time, logging
75-
logger = logging.getLogger(__name__)
50+
**NOTE** none of the functions being executed in any of the examples include information about the curses screen, multiprocessing or messaging queue - this is handled seamlessly by mpcurses.
7651

77-
def do_something(*args):
78-
group = args[0].get('group', 0)
79-
for _ in range(0, 600):
80-
logger.debug(f'processing item "[{group}]: {namegenerator.gen()}"')
81-
time.sleep(.01)
52+
Build the Docker image using the instructions below, run the examples. `python examples/##/sample.py`
8253

83-
MPcurses(
84-
function=do_something,
85-
process_data=[{'group': 1}, {'group': 2}, {'group': 3}],
86-
screen_layout={
87-
'display_item': {
88-
'position': (1, 1),
89-
'color': 14,
90-
'clear': True,
91-
'regex': r'^processing item "(?P<value>.*)"$',
92-
'table': True
93-
}
94-
}).execute()
95-
```
54+
#### [Prime Numbers Counter](https://github.com/soda480/mpcurses/blob/master/examples/03/sample.py)
9655

97-
Executing the code above results in the following:
98-
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example-multi.gif)
56+
Execute a function that calculates prime numbers for a set range of integers. Execution is scaled across 7 different workers where each process computes the primes for a different range of numbers. For example, the first worker computes primes for the range 1-10K, second worker computes for the range 10K-20K, etc. The main process keeps track of the number of prime numbers encountered for each worker and shows overall progress for each worker using a progress bar.
9957

100-
Serveral [examples](https://github.com/soda480/mpcurses/tree/master/examples) are included to help introduce the mpcurses library. Note the functions contained in all the examples are Python functions that have no context about multiprocessing or curses, they simply perform a function on a given dataset. Mpcurses takes care of setting up the multiprocessing, configuring the curses screen and maintaining the thread-safe queues that are required for inter-process communication.
58+
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example3.gif)
10159

102-
#### [example1](https://github.com/soda480/mpcurses/blob/master/examples/example1.py)
103-
Execute a function that processes a list of random items. The screen maintains indicators showing the number of items that have been processed. Two lists are maintained displaying the items that had errors and warnings.
104-
![example1](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example1.gif)
60+
#### [Item Processor](https://github.com/soda480/mpcurses/blob/master/examples/06/sample.py)
10561

106-
#### [example2](https://github.com/soda480/mpcurses/blob/master/examples/example2.py)
107-
Execute a function that processes a list of random items. Execution is scaled across three processes where each is responsible for processing items for a particular group. The screen maintains indicators displaying the items that had errors and warnings for each group.
108-
![example2](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example2.gif)
62+
Execute a function that processes a list of random items. Execution is scaled across 3 workers where each worker processes a unique set of items. The main process maintains indicators showing the number of items that have been processed by each worker; counting the number of Successful, Errors and Warnings. Three lists are also maintained, one for each group that list which specific items had Warnings and Failures.
10963

110-
#### [example3](https://github.com/soda480/mpcurses/blob/master/examples/example3.py)
111-
Execute a function that calculates prime numbers for a set range of integers. Execution is scaled across 10 different processes where each process computes the primes on a different set of numbers. For example, the first process computes primes for the set 1-10K, second process 10K-20K, third process 20K-30K, etc. The screen keeps track of the number of prime numbers encountered for each set and maintains a progress bar for each process.
112-
![example3](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example3.gif)
64+
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example6.gif)
11365

114-
#### Running the examples
66+
#### [Bay Enclosure Firmware Update](https://github.com/soda480/mpcurses/blob/master/examples/09/sample.py)
11567

116-
Build the Docker image and run the Docker container using the instructions described in the [Development](#development) section. Run the example scripts within the container:
68+
Execute a function that contains a workflow containing tasks to update firmware on a server residing in a blade enclosure. Execution is scaled across a worker pool with five active workers. The main process updates the screen showing status of each worker as they execute the workflow tasks for each blade server.
11769

118-
```bash
119-
python examples/example#.py
120-
```
70+
![example](https://raw.githubusercontent.com/soda480/mpcurses/master/docs/images/example9.gif)
12171

12272
### Projects using `mpcurses`
12373

build.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
use_plugin('pypi:pybuilder_anybadge')
2828

2929
name = 'mpcurses'
30-
authors = [Author('Emilio Reyes', 'emilio.reyes@intel.com')]
31-
summary = 'Mpcurses is an abstraction of the Python curses and multiprocessing libraries providing function execution and runtime visualization capabilities'
30+
authors = [Author('Emilio Reyes', 'soda480@gmail.com')]
31+
summary = 'The mpcurses module facilitates seamless terminal screen updates from child processes within a multiprocessing worker pool, leveraging the curses library for terminal manipulation'
3232
url = 'https://github.com/soda480/mpcurses'
33-
version = '0.5.0'
33+
version = '1.0.0'
3434
default_task = [
3535
'clean',
3636
'analyze',
@@ -64,7 +64,7 @@ def set_properties(project):
6464
'Programming Language :: Python :: 3.10',
6565
'Programming Language :: Python :: 3.11',
6666
'Programming Language :: Python :: 3.12'])
67-
project.set_property('radon_break_build_average_complexity_threshold', 4)
67+
project.set_property('radon_break_build_average_complexity_threshold', 5)
6868
project.set_property('radon_break_build_complexity_threshold', 14)
6969
project.set_property('bandit_break_build', True)
7070
project.set_property('anybadge_exclude', 'coverage, complexity')

docs/images/demo.gif

30.3 KB
Loading

docs/images/example-multi.gif

-57.8 KB
Binary file not shown.

docs/images/example.gif

-92.5 KB
Binary file not shown.

docs/images/example1.gif

-245 KB
Binary file not shown.

docs/images/example2.gif

-737 KB
Binary file not shown.

docs/images/example3.gif

-616 KB
Loading

docs/images/example4.gif

-84.1 KB
Binary file not shown.

docs/images/example5.gif

-136 KB
Binary file not shown.

docs/images/example6.gif

711 KB
Loading

docs/images/example9.gif

495 KB
Loading

examples/01/layout.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
screen_layout = {
2+
'range_header': {
3+
'position': (2, 5), 'text': 'Range', 'text_color': 14 },
4+
'prime_header': {
5+
'position': (2, 14), 'text': 'Prime', 'text_color': 14 },
6+
'number': {
7+
'position': (3, 2), 'color': 15, 'zfill': 5,
8+
'regex': r'^checking number (?P<value>\d+)$' },
9+
'upper': {
10+
'position': (3, 7), 'color': 27,
11+
'regex': r'^checking primes between \d+(?P<value>/\d+)$' },
12+
'prime': {
13+
'position': (3, 14), 'color': 2, 'keep_count': True, 'zfill': 4,
14+
'regex': r'^\d* is prime$' }
15+
}

examples/01/sample.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from primes import count_primes
2+
from layout import screen_layout
3+
from mpcurses import MPcurses
4+
5+
def main():
6+
MPcurses(
7+
function=count_primes,
8+
process_data=[
9+
{'nrange': '1-10000'}],
10+
screen_layout=screen_layout).execute()
11+
12+
if __name__ == '__main__':
13+
main()

examples/02/layout.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
screen_layout = {
2+
'range_header': {
3+
'position': (2, 5), 'text': 'Range', 'text_color': 14 },
4+
'prime_header': {
5+
'position': (2, 14), 'text': 'Prime', 'text_color': 14 },
6+
'number': {
7+
'position': (3, 2), 'color': 15, 'table': True,
8+
'regex': r'^checking number (?P<value>\d+)$' },
9+
'upper': {
10+
'position': (3, 7), 'color': 27, 'table': True,
11+
'regex': r'^checking primes between \d+(?P<value>/\d+)$' },
12+
'prime': {
13+
'position': (3, 14), 'color': 2,'keep_count': True, 'zfill': 4,
14+
'regex': r'^\d* is prime$' },
15+
'_counter_': {
16+
'position': (3, 19), 'color': 15, 'modulus': 125, 'counter_text': chr(9632),
17+
'categories': ['number'] }
18+
}

examples/02/sample.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from primes import count_primes
2+
from layout import screen_layout
3+
from mpcurses import MPcurses
4+
5+
def main():
6+
MPcurses(
7+
function=count_primes,
8+
process_data=[
9+
{'nrange': '1-10000'}],
10+
screen_layout=screen_layout).execute()
11+
12+
if __name__ == '__main__':
13+
main()

examples/03/layout.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
screen_layout = {
2+
'range_header': {
3+
'position': (2, 5), 'text': 'Range', 'text_color': 14 },
4+
'prime_header': {
5+
'position': (2, 14), 'text': 'Prime', 'text_color': 14 },
6+
'number': {
7+
'position': (3, 2), 'color': 15, 'table': True,
8+
'regex': r'^checking number (?P<value>\d+)$' },
9+
'upper': {
10+
'position': (3, 7), 'color': 27, 'table': True,
11+
'regex': r'^checking primes between \d+(?P<value>/\d+)$' },
12+
'prime': {
13+
'position': (3, 14), 'color': 2, 'table': True, 'keep_count': True, 'zfill': 4,
14+
'regex': r'^\d* is prime$' },
15+
'_counter_': {
16+
'position': (3, 19), 'color': 15, 'table': True, 'modulus': 125, 'counter_text': chr(9632),
17+
'categories': ['number'] },
18+
'total_header': {
19+
'position': (6, 7), 'text': 'Total:', 'text_color': 14 },
20+
'total': {
21+
'position': (6, 14), 'color': 2, 'keep_count': True, 'zfill': 4,
22+
'regex': r'^\d* is prime$' },
23+
}

examples/03/sample.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from primes import count_primes
2+
from layout import screen_layout
3+
from mpcurses import MPcurses
4+
5+
def main():
6+
MPcurses(
7+
function=count_primes,
8+
process_data=[
9+
{'nrange': '00001-10000'},
10+
{'nrange': '10001-20000'},
11+
{'nrange': '20001-30000'},
12+
{'nrange': '30001-40000'},
13+
{'nrange': '40001-50000'},
14+
{'nrange': '50001-60000'},
15+
{'nrange': '60001-70000'}],
16+
screen_layout=screen_layout).execute()
17+
18+
if __name__ == '__main__':
19+
main()

examples/04/layout.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
screen_layout = {
3+
'total': {
4+
'position': (1, 6),
5+
'text': 'Total:',
6+
'text_color': 0,
7+
'color': 15,
8+
'regex': r'^(?P<value>\d+) total items$'
9+
},
10+
'pass': {
11+
'position': (2, 7),
12+
'text': 'Pass:',
13+
'text_color': 0,
14+
'color': 2,
15+
'keep_count': True,
16+
'regex': r'^item ".*" was processed$'
17+
},
18+
'warn': {
19+
'position': (3, 7),
20+
'text': 'Warn:',
21+
'text_color': 0,
22+
'color': 3, # 232,
23+
'keep_count': True,
24+
'regex': r'^warning processing item ".*"$'
25+
},
26+
'fail': {
27+
'position': (4, 7),
28+
'text': 'Fail:',
29+
'text_color': 0,
30+
'color': 1, # 237,
31+
'keep_count': True,
32+
'regex': r'^error processing item ".*"$'
33+
},
34+
'processing': {
35+
'position': (5, 1),
36+
'text': 'Processing:',
37+
'text_color': 0,
38+
'color': 14,
39+
'clear': True,
40+
'regex': r'^processing item "(?P<value>.*)"$'
41+
},
42+
'processing_done': {
43+
'position': (5, 1),
44+
'replace_text': ' ',
45+
'clear': True,
46+
'regex': r'^processing complete$'
47+
},
48+
'_counter_': {
49+
'position': (6, 0),
50+
'categories': [
51+
'pass',
52+
'warn',
53+
'fail'
54+
],
55+
'counter_text': chr(9632),
56+
'width': 100
57+
}
58+
}

0 commit comments

Comments
 (0)