├── .gitignore ├── LICENSE ├── README.md ├── examples ├── country_stats.py ├── downloader.py ├── late_add_col.py ├── primes_1_simple.py ├── primes_2_formatting.py └── primes_3_incremental.py ├── scolp.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.pyc 3 | *~ 4 | *.log 5 | *.tmp 6 | 7 | # PyCharm/IDEA files 8 | .idea/* 9 | **/.idea/**/dataSources/ 10 | **/.idea/**/dataSources.ids 11 | **/.idea/**/dataSources.xml 12 | **/.idea/**/dataSources.local.xml 13 | **/.idea/**/sqlDataSources.xml 14 | **/.idea/**/dynamic.xml 15 | **/.idea/**/uiDesigner.xml 16 | **/.idea/**/gradle.xml 17 | **/.idea/**/libraries 18 | 19 | # User-specific stuff: 20 | **/.idea/**/workspace.xml 21 | **/.idea/**/tasks.xml 22 | **/.idea/**/misc.xml 23 | *.iws 24 | 25 | dist/ 26 | build/ 27 | *egg-info* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 David Ohana 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scolp 2 | 3 | ## Introduction 4 | 5 | Scolp is Streaming Column Printer for Python 3.6 or later. 6 | 7 | Scolp let you easily pretty-print masses of tabular data in a streaming fashion - each value is printed when available, without waiting for end of data. It is perfect for apps that need to print progress reports in columns. 8 | 9 | Main features: 10 | 11 | * Auto-adjusting column width according to the largest value so far or column header width. 12 | 13 | * Control verbosity of printing by: 14 | - printing ``1`` of each ``n`` rows 15 | - printing no more than ``1`` row per ``n`` seconds 16 | 17 | * Control format of printed values by: 18 | - setting global defaults 19 | - setting defaults per variable type (``int``, ``float``, ``str``, ``datetime``) 20 | - setting explicit formatting per column 21 | 22 | * Control alignment of printed values: 23 | - left 24 | - right 25 | - center 26 | - auto: numbers to the right, strings or other types to the left. 27 | 28 | * Control cosmetics of columns (initial width, padding fill char, alignment, and more..) by: 29 | - setting global defaults 30 | - setting explicit formatting per column 31 | 32 | * Control column title printing style: 33 | - Inline in each row 34 | - As headers, repeating each n rows 35 | 36 | * Easily print row count or time since execution started without need to keep track of those values yourself. 37 | 38 | ## Examples 39 | 40 | #### Example 1 41 | 42 | Lets start with a simple country statistics output using default settings: 43 | 44 | ```python 45 | import scolp 46 | 47 | scolper = scolp.Scolp() 48 | scolper.config.add_columns("country", "population (mil)", "capital city", "life expectancy (female)", 49 | "life expectancy (male)", "fertility rate") 50 | scolper.print("Netherlands", 16.81, "Amsterdam", 83, 79, 1.5, 51 | "China", 1350.0, "Beijing", 76, 72, 1.8, 52 | "Israel", 7.71, "Jerusalem", 84, 80, 2.7, 53 | "Nigeria") 54 | scolper.print(174.51) 55 | ``` 56 | 57 | Output: 58 | 59 | (Note how column width is auto adjusting, line breaks are printed automatically after last column, and each value is printed immediately without waiting for end of row) 60 | 61 | ``` 62 | country |population (mil)|capital city|life expectancy (female)|life expectancy (male)|fertility rate 63 | --------|----------------|------------|------------------------|----------------------|-------------- 64 | Netherlands| 16.810|Amsterdam | 83| 79| 1.500 65 | 66 | country |population (mil)|capital city|life expectancy (female)|life expectancy (male)|fertility rate 67 | -----------|----------------|------------|------------------------|----------------------|-------------- 68 | China | 1,350.000|Beijing | 76| 72| 1.800 69 | Israel | 7.710|Jerusalem | 84| 80| 2.700 70 | Nigeria | 174.510| 71 | ``` 72 | 73 | #### Example 2 74 | 75 | Lets build a program that find prime numbers. We will print the count of primes 76 | we found so far and the last prime found. 77 | 78 | ```python 79 | import datetime, scolp 80 | 81 | def is_prime(num): 82 | return 2 in [num, 2 ** num % num] 83 | 84 | scolper = scolp.Scolp() 85 | scolper.config.add_columns("time", "elapsed", "inspected_count", "prime_count", "last", "progress %") 86 | scolper.config.output_each_n_seconds = 1 87 | 88 | prime_count = 0 89 | last_prime = None 90 | i = 9_999_800 91 | target_count = 30 92 | while prime_count < target_count: 93 | if is_prime(i): 94 | last_prime = i 95 | prime_count += 1 96 | progress = prime_count / target_count * 100 97 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), 98 | scolper.row_index + 1, prime_count, last_prime, progress) 99 | i += 1 100 | 101 | ``` 102 | 103 | Output: 104 | 105 | (Note how the header repeats, the column width auto-expanding and the numbers are aligned to the right) 106 | 107 | ``` 108 | time |elapsed |inspected_count|prime_count|last |progress % 109 | --------|--------|---------------|-----------|--------|---------- 110 | 2019-06-05 11:49:31.271191|0:00:00 | 1| 0|None | 0.000 111 | 112 | time |elapsed |inspected_count|prime_count|last |progress % 113 | --------------------------|--------|---------------|-----------|--------|---------- 114 | 2019-06-05 11:49:32.306225|0:00:01 | 27| 1|9,999,823| 3.333 115 | 2019-06-05 11:49:33.325694|0:00:02 | 53| 1|9,999,823| 3.333 116 | 2019-06-05 11:49:34.341678|0:00:03 | 79| 3|9,999,877| 10.000 117 | 2019-06-05 11:49:35.378966|0:00:04 | 105| 6|9,999,901| 20.000 118 | 2019-06-05 11:49:36.399298|0:00:05 | 131| 8|9,999,929| 26.667 119 | 2019-06-05 11:49:37.415522|0:00:06 | 157| 11|9,999,943| 36.667 120 | 2019-06-05 11:49:38.450551|0:00:07 | 183| 13|9,999,973| 43.333 121 | 2019-06-05 11:49:39.478987|0:00:08 | 209| 14|9,999,991| 46.667 122 | 2019-06-05 11:49:40.485409|0:00:09 | 233| 15|10,000,019| 50.000 123 | 124 | time |elapsed |inspected_count|prime_count|last |progress % 125 | --------------------------|--------|---------------|-----------|----------|---------- 126 | 2019-06-05 11:49:41.508298|0:00:10 | 259| 15|10,000,019| 50.000 127 | 2019-06-05 11:49:42.543115|0:00:11 | 283| 16|10,000,079| 53.333 128 | 2019-06-05 11:49:43.555733|0:00:12 | 306| 17|10,000,103| 56.667 129 | 2019-06-05 11:49:44.572379|0:00:13 | 328| 18|10,000,121| 60.000 130 | 2019-06-05 11:49:45.574066|0:00:14 | 349| 20|10,000,141| 66.667 131 | 2019-06-05 11:49:46.583462|0:00:15 | 372| 21|10,000,169| 70.000 132 | 2019-06-05 11:49:47.594724|0:00:16 | 396| 22|10,000,189| 73.333 133 | 2019-06-05 11:49:48.639124|0:00:17 | 420| 22|10,000,189| 73.333 134 | 2019-06-05 11:49:49.661211|0:00:18 | 441| 24|10,000,229| 80.000 135 | 2019-06-05 11:49:50.691037|0:00:19 | 463| 27|10,000,261| 90.000 136 | 137 | time |elapsed |inspected_count|prime_count|last |progress % 138 | --------------------------|--------|---------------|-----------|----------|---------- 139 | 2019-06-05 11:49:51.721844|0:00:20 | 487| 28|10,000,271| 93.333 140 | 2019-06-05 11:49:52.733437|0:00:22 | 510| 29|10,000,303| 96.667 141 | 2019-06-05 11:49:53.750463|0:00:23 | 534| 29|10,000,303| 96.667 142 | ``` 143 | 144 | #### Example 3 145 | 146 | Now, lets change the code of the previous example to add a bit of custom formatting: 147 | 148 | ```python 149 | scolper = scolp.Scolp() 150 | scolper.config.add_column("time", width=20) 151 | scolper.config.add_columns("elapsed", 152 | "inspected_count", 153 | "prime_count") 154 | scolper.config.add_column("last", width=11) 155 | scolper.config.add_column("progress", fmt="{:.1%}") 156 | scolper.config.output_each_n_seconds = 1 157 | scolper.config.header_repeat_row_count_first = 0 158 | scolper.config.default_column.column_separator = " " 159 | scolper.config.default_column.type_to_format[datetime.datetime] = "{:%Y-%m-%d %H:%M:%S}" 160 | 161 | prime_count = 0 162 | last_prime = None 163 | i = 9_999_800 164 | target_count = 30 165 | while prime_count < target_count: 166 | if is_prime(i): 167 | last_prime = i 168 | prime_count += 1 169 | progress = prime_count / target_count 170 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), 171 | scolper.row_index + 1, prime_count, last_prime, progress) 172 | i += 1 173 | ``` 174 | 175 | Output: 176 | 177 | ``` 178 | time elapsed inspected_count prime_count last progress 179 | -------------------- -------- --------------- ----------- ----------- -------- 180 | 2019-06-05 11:54:46 0:00:00 1 0 None 0.0% 181 | 2019-06-05 11:54:47 0:00:01 23 0 None 0.0% 182 | 2019-06-05 11:54:48 0:00:02 45 1 9,999,823 3.3% 183 | 2019-06-05 11:54:49 0:00:03 67 2 9,999,863 6.7% 184 | 2019-06-05 11:54:50 0:00:04 90 5 9,999,889 16.7% 185 | 2019-06-05 11:54:51 0:00:05 115 7 9,999,907 23.3% 186 | 2019-06-05 11:54:52 0:00:06 139 10 9,999,937 33.3% 187 | 2019-06-05 11:54:53 0:00:07 164 11 9,999,943 36.7% 188 | 2019-06-05 11:54:54 0:00:08 188 13 9,999,973 43.3% 189 | 2019-06-05 11:54:55 0:00:09 212 14 9,999,991 46.7% 190 | 191 | time elapsed inspected_count prime_count last progress 192 | -------------------- -------- --------------- ----------- ----------- -------- 193 | 2019-06-05 11:54:56 0:00:10 237 15 10,000,019 50.0% 194 | 2019-06-05 11:54:57 0:00:11 261 15 10,000,019 50.0% 195 | 2019-06-05 11:54:58 0:00:12 284 16 10,000,079 53.3% 196 | 2019-06-05 11:54:59 0:00:13 308 17 10,000,103 56.7% 197 | 2019-06-05 11:55:00 0:00:14 331 18 10,000,121 60.0% 198 | 2019-06-05 11:55:01 0:00:15 355 20 10,000,141 66.7% 199 | 2019-06-05 11:55:02 0:00:16 379 21 10,000,169 70.0% 200 | 2019-06-05 11:55:03 0:00:17 403 22 10,000,189 73.3% 201 | 2019-06-05 11:55:04 0:00:18 426 23 10,000,223 76.7% 202 | 2019-06-05 11:55:05 0:00:20 448 25 10,000,247 83.3% 203 | 204 | time elapsed inspected_count prime_count last progress 205 | -------------------- -------- --------------- ----------- ----------- -------- 206 | 2019-06-05 11:55:06 0:00:21 471 27 10,000,261 90.0% 207 | 2019-06-05 11:55:07 0:00:22 493 28 10,000,271 93.3% 208 | 2019-06-05 11:55:08 0:00:23 516 29 10,000,303 96.7% 209 | 2019-06-05 11:55:09 0:00:24 539 29 10,000,303 96.7% 210 | ``` 211 | 212 | #### Example 4 213 | 214 | Lets build an HTTP big-file downloader. 215 | 216 | ```python 217 | import datetime, urllib3, scolp 218 | 219 | url = "http://speedtest.tele2.net/100MB.zip" 220 | path = "downloaded.tmp" 221 | chunk_size_bytes = 1000 222 | 223 | scolp_cfg = scolp.Config() 224 | scolp_cfg.add_column("time", fmt="{:%H:%M:%S}") 225 | scolp_cfg.add_column("elapsed") 226 | scolp_cfg.add_column("downloaded", width=16, fmt="{:,} B") 227 | scolp_cfg.add_column("speed", width=14, pad_align=scolp.Alignment.RIGHT, type_to_format={float: "{:,.1f} kB/s"}) 228 | 229 | scolp_cfg.output_each_n_seconds = 1 230 | scolp_cfg.title_mode = scolp.TitleMode.INLINE 231 | scolp_cfg.default_column.column_separator = " | " 232 | 233 | scolper = scolp.Scolp(scolp_cfg) 234 | 235 | http = urllib3.PoolManager() 236 | r = http.request('GET', url, preload_content=False) 237 | 238 | dl_bytes = 0 239 | 240 | 241 | def progress_update(): 242 | elapsed_sec = scolper.elapsed_since_init().total_seconds() 243 | speed_kbps = dl_bytes / elapsed_sec / 1000 if elapsed_sec > 0 else "unknown" 244 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), dl_bytes, speed_kbps) 245 | 246 | 247 | with open(path, 'wb') as out: 248 | while True: 249 | data = r.read(chunk_size_bytes) 250 | if not data: 251 | break 252 | out.write(data) 253 | dl_bytes += len(data) 254 | progress_update() 255 | 256 | scolper.force_print_next_row() 257 | progress_update() 258 | r.release_conn() 259 | 260 | ``` 261 | 262 | Output: 263 | 264 | ``` 265 | time=14:30:11 | elapsed=0:00:00 | downloaded= 1,000 B | speed= unknown 266 | time=14:30:12 | elapsed=0:00:01 | downloaded= 801,000 B | speed= 801.0 kB/s 267 | time=14:30:13 | elapsed=0:00:02 | downloaded= 1,743,000 B | speed= 871.5 kB/s 268 | time=14:30:14 | elapsed=0:00:03 | downloaded= 2,758,000 B | speed= 919.3 kB/s 269 | time=14:30:15 | elapsed=0:00:04 | downloaded= 3,779,000 B | speed= 944.8 kB/s 270 | time=14:30:16 | elapsed=0:00:05 | downloaded= 4,794,000 B | speed= 958.8 kB/s 271 | time=14:30:17 | elapsed=0:00:06 | downloaded= 5,809,000 B | speed= 968.2 kB/s 272 | time=14:30:18 | elapsed=0:00:07 | downloaded= 6,824,000 B | speed= 974.9 kB/s 273 | time=14:30:19 | elapsed=0:00:08 | downloaded= 7,839,000 B | speed= 979.9 kB/s 274 | time=14:30:20 | elapsed=0:00:09 | downloaded= 8,857,000 B | speed= 984.1 kB/s 275 | time=14:30:21 | elapsed=0:00:10 | downloaded= 9,799,000 B | speed= 979.9 kB/s 276 | time=14:30:22 | elapsed=0:00:11 | downloaded= 10,814,000 B | speed= 983.1 kB/s 277 | time=14:30:23 | elapsed=0:00:12 | downloaded= 11,838,000 B | speed= 986.5 kB/s 278 | time=14:30:24 | elapsed=0:00:13 | downloaded= 12,855,000 B | speed= 988.8 kB/s 279 | time=14:30:25 | elapsed=0:00:14 | downloaded= 13,870,000 B | speed= 990.7 kB/s 280 | time=14:30:26 | elapsed=0:00:15 | downloaded= 14,891,000 B | speed= 992.7 kB/s 281 | time=14:30:27 | elapsed=0:00:16 | downloaded= 15,906,000 B | speed= 994.1 kB/s 282 | time=14:30:28 | elapsed=0:00:18 | downloaded= 25,600,000 B | speed= 1,422.2 kB/s 283 | time=14:30:29 | elapsed=0:00:19 | downloaded= 37,146,000 B | speed= 1,955.1 kB/s 284 | time=14:30:30 | elapsed=0:00:20 | downloaded= 47,847,000 B | speed= 2,392.3 kB/s 285 | time=14:30:31 | elapsed=0:00:21 | downloaded= 60,962,000 B | speed= 2,903.0 kB/s 286 | time=14:30:32 | elapsed=0:00:22 | downloaded= 72,931,000 B | speed= 3,315.0 kB/s 287 | time=14:30:33 | elapsed=0:00:23 | downloaded= 85,094,000 B | speed= 3,699.7 kB/s 288 | time=14:30:34 | elapsed=0:00:24 | downloaded= 104,857,600 B | speed= 4,369.1 kB/s 289 | ``` 290 | 291 | 292 | ## Requirements 293 | 294 | Scolp has no 3rd party requirements other than Python 3.6 or later. 295 | 296 | 297 | ## Getting Started 298 | 299 | Scolp is available via PyPi and can be installed using: 300 | 301 | ```pip install scolp```. 302 | 303 | ## Todo 304 | 305 | * Document public API of the library 306 | * Support colors -------------------------------------------------------------------------------- /examples/country_stats.py: -------------------------------------------------------------------------------- 1 | import scolp 2 | 3 | scolper = scolp.Scolp() 4 | scolper.config.add_columns("country", "population (mil)", "capital city", "life expectancy (female)", 5 | "life expectancy (male)", "fertility rate") 6 | scolper.print("Netherlands", 16.81, "Amsterdam", 83, 79, 1.5, 7 | "China", 1350.0, "Beijing", 76, 72, 1.8, 8 | "Israel", 7.71, "Jerusalem", 84, 80, 2.7, 9 | "Nigeria") 10 | scolper.print(174.51) 11 | 12 | -------------------------------------------------------------------------------- /examples/downloader.py: -------------------------------------------------------------------------------- 1 | import datetime, urllib3, scolp 2 | 3 | url = "http://speedtest.tele2.net/100MB.zip" 4 | path = "downloaded.tmp" 5 | chunk_size_bytes = 1000 6 | 7 | scolp_cfg = scolp.Config() 8 | scolp_cfg.add_column("time", fmt="{:%H:%M:%S}") 9 | scolp_cfg.add_column("elapsed") 10 | scolp_cfg.add_column("downloaded", width=16, fmt="{:,} B") 11 | scolp_cfg.add_column("speed", width=14, pad_align=scolp.Alignment.RIGHT, type_to_format={float: "{:,.1f} kB/s"}) 12 | 13 | scolp_cfg.output_each_n_seconds = 1 14 | scolp_cfg.title_mode = scolp.TitleMode.INLINE 15 | scolp_cfg.default_column.column_separator = " | " 16 | 17 | scolper = scolp.Scolp(scolp_cfg) 18 | 19 | http = urllib3.PoolManager() 20 | r = http.request('GET', url, preload_content=False) 21 | 22 | dl_bytes = 0 23 | 24 | 25 | def progress_update(): 26 | elapsed_sec = scolper.elapsed_since_init().total_seconds() 27 | speed_kbps = dl_bytes / elapsed_sec / 1000 if elapsed_sec > 0 else "unknown" 28 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), dl_bytes, speed_kbps) 29 | 30 | 31 | with open(path, 'wb') as out: 32 | while True: 33 | data = r.read(chunk_size_bytes) 34 | if not data: 35 | break 36 | out.write(data) 37 | dl_bytes += len(data) 38 | progress_update() 39 | 40 | scolper.force_print_next_row() 41 | progress_update() 42 | r.release_conn() 43 | -------------------------------------------------------------------------------- /examples/late_add_col.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import scolp 4 | 5 | s = scolp.Scolp() 6 | s.config.add_columns('date', 'a') 7 | s.config.default_column.type_to_format[datetime.datetime] = '{:%c %Z}' 8 | s.print(datetime.datetime.utcnow(), 'foo') 9 | s.print(datetime.datetime.utcnow(), 'bar') 10 | 11 | s.config.add_column('b') 12 | s.print_col_headers() 13 | s.print(datetime.datetime.utcnow(), 'fizz', 'buzz') 14 | s.print(datetime.datetime.utcnow(), 'tom', 'jerry') 15 | 16 | -------------------------------------------------------------------------------- /examples/primes_1_simple.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import scolp 4 | 5 | 6 | def is_prime(num): 7 | return 2 in [num, 2 ** num % num] 8 | 9 | 10 | scolper = scolp.Scolp() 11 | scolper.config.add_columns("time", "elapsed", "inspected_count", "prime_count", "last", "progress %") 12 | scolper.config.output_each_n_seconds = 1 13 | 14 | prime_count = 0 15 | last_prime = None 16 | i = 9_999_800 17 | target_count = 30 18 | while prime_count < target_count: 19 | if is_prime(i): 20 | last_prime = i 21 | prime_count += 1 22 | progress = prime_count / target_count * 100 23 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), 24 | scolper.row_index + 1, prime_count, last_prime, progress) 25 | i += 1 26 | -------------------------------------------------------------------------------- /examples/primes_2_formatting.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import scolp 4 | 5 | 6 | def is_prime(num): 7 | return 2 in [num, 2 ** num % num] 8 | 9 | 10 | scolper = scolp.Scolp() 11 | scolper.config.add_column("time", width=20) 12 | scolper.config.add_columns("elapsed", 13 | "inspected_count", 14 | "prime_count") 15 | scolper.config.add_column("last", width=11) 16 | scolper.config.add_column("progress", fmt="{:.1%}") 17 | scolper.config.output_each_n_seconds = 1 18 | scolper.config.header_repeat_row_count_first = 0 19 | scolper.config.default_column.column_separator = " " 20 | scolper.config.default_column.type_to_format[datetime.datetime] = "{:%Y-%m-%d %H:%M:%S}" 21 | 22 | prime_count = 0 23 | last_prime = None 24 | i = 9_999_800 25 | target_count = 30 26 | while prime_count < target_count: 27 | if is_prime(i): 28 | last_prime = i 29 | prime_count += 1 30 | progress = prime_count / target_count 31 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), 32 | scolper.row_index + 1, prime_count, last_prime, progress) 33 | i += 1 34 | -------------------------------------------------------------------------------- /examples/primes_3_incremental.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import scolp 4 | 5 | 6 | def is_prime(num): 7 | return 2 in [num, 2 ** num % num] 8 | 9 | 10 | scolp_cfg = scolp.Config() 11 | scolp_cfg.add_column("time", width=20) 12 | scolp_cfg.add_columns("elapsed", 13 | "inspected_count", 14 | "prime_count") 15 | scolp_cfg.add_column("last", width=11) 16 | scolp_cfg.add_column("progress", fmt="{:.1%}") 17 | scolp_cfg.header_repeat_row_count_first = 0 18 | scolp_cfg.default_column.type_to_format[datetime.datetime] = "{:%Y-%m-%d %H:%M:%S}" 19 | scolper = scolp.Scolp(scolp_cfg) 20 | 21 | prime_count = 0 22 | last_prime = None 23 | i = 500_000_000 24 | target_count = 30 25 | while prime_count < target_count: 26 | scolper.print(datetime.datetime.now(), scolper.elapsed_since_init(), scolper.row_index + 1) 27 | if is_prime(i): 28 | last_prime = i 29 | prime_count += 1 30 | progress = prime_count / target_count 31 | scolper.print(prime_count, last_prime, progress) 32 | i += 1 33 | -------------------------------------------------------------------------------- /scolp.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import numbers 3 | from enum import Enum, auto 4 | from typing import List, Dict 5 | 6 | 7 | class TitleMode(Enum): 8 | INLINE = auto() 9 | HEADER = auto() 10 | NONE = auto() 11 | 12 | 13 | class Alignment(Enum): 14 | LEFT = auto() 15 | RIGHT = auto() 16 | CENTER = auto() 17 | AUTO = auto() 18 | 19 | 20 | class Column: 21 | # noinspection PyTypeChecker 22 | def __init__(self): 23 | self.title = "" 24 | self.format: str = None 25 | self.width: int = None 26 | self.title_to_value_separator: str = None 27 | self.pad_fill_char: str = None 28 | self.pad_align: str = None 29 | self.column_separator: str = None 30 | self.type_to_format: Dict[type, str] = None 31 | 32 | 33 | class Config: 34 | 35 | def __init__(self): 36 | self.columns: List[Column] = [] 37 | 38 | self.output_each_n_rows = 1 39 | self.output_each_n_seconds = 0 40 | 41 | self.title_mode = TitleMode.HEADER 42 | self.header_repeat_row_count = 10 43 | self.header_repeat_row_count_first = 1 44 | self.header_line_char = "-" 45 | 46 | self.default_column = Column() 47 | self.default_column.width = 8 48 | self.default_column.format = None 49 | self.default_column.title_to_value_separator = "=" 50 | self.default_column.pad_fill_char = " " 51 | self.default_column.column_separator = "|" 52 | self.default_column.pad_align = Alignment.AUTO 53 | self.default_column.type_to_format = { 54 | int: "{:,}", 55 | float: "{:,.3f}", 56 | } 57 | 58 | self.print_func = self._print_impl 59 | 60 | def add_column(self, 61 | title: str, fmt=None, width=None, 62 | title_to_value_separator=None, pad_fill_char=None, pad_align=None, 63 | column_separator=None, type_to_format=None): 64 | col = Column() 65 | col.title = title 66 | col.format = fmt 67 | col.width = width 68 | col.title_to_value_separator = title_to_value_separator 69 | col.pad_fill_char = pad_fill_char 70 | col.pad_align = pad_align 71 | col.type_to_format = type_to_format 72 | col.column_separator = column_separator 73 | self.columns.append(col) 74 | return self 75 | 76 | def add_columns(self, *titles: str): 77 | for title in titles: 78 | self.add_column(title) 79 | 80 | @staticmethod 81 | def _print_impl(s: str): 82 | print(s, end="", flush=True) 83 | 84 | 85 | class Scolp: 86 | 87 | def __init__(self, config=Config()): 88 | self.config = config 89 | self.row_index = 0 90 | self.last_row_print_time_seconds = 0 91 | self.init_time = datetime.datetime.now() 92 | self._cur_col_index = 0 93 | self._cur_printed_row_index = -1 94 | self._enable_print_current_row = False 95 | self._force_print_row_index = 0 96 | 97 | def _print(self, s: str): 98 | self.config.print_func(s) 99 | 100 | def _println(self): 101 | self._print("\n") 102 | 103 | def _pad(self, s: str, col: Column, orig_value): 104 | width = self._get_config_param(col, "width") 105 | 106 | col.width = max(width, len(s)) 107 | 108 | if len(s) == col.width: 109 | return s 110 | 111 | pad_fill_char = self._get_config_param(col, "pad_fill_char") 112 | 113 | align = self._get_config_param(col, "pad_align") 114 | if align == Alignment.AUTO and orig_value is not None: 115 | if isinstance(orig_value, numbers.Number): 116 | align = Alignment.RIGHT 117 | else: 118 | align = Alignment.LEFT 119 | 120 | if align == Alignment.RIGHT: 121 | padded = str.rjust(s, width, pad_fill_char) 122 | elif align == Alignment.CENTER: 123 | padded = str.center(s, width, pad_fill_char) 124 | else: 125 | padded = str.ljust(s, width, pad_fill_char) 126 | 127 | return padded 128 | 129 | def get_default_format_str(self, col: Column, value): 130 | type_to_format = self._get_config_param(col, "type_to_format") 131 | for typ, fmt in type_to_format.items(): 132 | if isinstance(value, typ): 133 | return fmt 134 | return None 135 | 136 | def _format(self, col: Column, value): 137 | fmt = self._get_config_param(col, "format") 138 | if fmt is None: 139 | fmt = self.get_default_format_str(col, value) 140 | 141 | if fmt is None: 142 | fmt_val = str(value) 143 | else: 144 | try: 145 | fmt_val = str.format(fmt, value) 146 | except (ValueError, TypeError): 147 | fmt_val = str(value) + " (FMT_ERR)" 148 | 149 | fmt_val = self._pad(fmt_val, col, value) 150 | return fmt_val 151 | 152 | def _get_config_param(self, col: Column, param_name: str): 153 | col_param = col.__dict__[param_name] 154 | if col_param is not None: 155 | return col_param 156 | return self.config.default_column.__dict__[param_name] 157 | 158 | def print_col_headers(self): 159 | self._println() 160 | for col in self.config.columns: 161 | title = self._pad(col.title, col, None) 162 | self._print(title) 163 | if col is not self.config.columns[-1]: 164 | column_separator = self._get_config_param(col, "column_separator") 165 | self._print(column_separator) 166 | self._println() 167 | 168 | for col in self.config.columns: 169 | horz_line = self.config.header_line_char * col.width 170 | self._print(horz_line) 171 | if col is not self.config.columns[-1]: 172 | column_separator = self._get_config_param(col, "column_separator") 173 | self._print(column_separator) 174 | self._println() 175 | 176 | def _print_column(self, var_value): 177 | col = self.config.columns[self._cur_col_index] 178 | 179 | if self._cur_col_index == 0: 180 | self._cur_printed_row_index += 1 181 | self.last_row_print_time_seconds = datetime.datetime.now().timestamp() 182 | 183 | if self.config.title_mode == TitleMode.HEADER and \ 184 | (self._cur_printed_row_index == self.config.header_repeat_row_count_first or 185 | self._cur_printed_row_index % self.config.header_repeat_row_count == 0): 186 | self.print_col_headers() 187 | 188 | if self.config.title_mode == TitleMode.INLINE and col.title and not col.title.isspace(): 189 | self._print(col.title) 190 | title_to_value_separator = self._get_config_param(col, "title_to_value_separator") 191 | self._print(title_to_value_separator) 192 | 193 | fmt_val = self._format(col, var_value) 194 | 195 | self._print(fmt_val) 196 | 197 | if self._cur_col_index == len(self.config.columns) - 1: 198 | self._println() 199 | else: 200 | column_separator = self._get_config_param(col, "column_separator") 201 | self._print(column_separator) 202 | 203 | def _update_print_enable_status(self): 204 | if self._cur_col_index != 0: 205 | return 206 | 207 | self._enable_print_current_row = \ 208 | self.row_index == self._force_print_row_index or \ 209 | self.row_index % self.config.output_each_n_rows == 0 and \ 210 | datetime.datetime.now().timestamp() - self.last_row_print_time_seconds >= self.config.output_each_n_seconds 211 | 212 | def print(self, *var_values): 213 | if len(self.config.columns) == 0: 214 | self.config.add_column("(no title)") 215 | 216 | for var_value in var_values: 217 | self._update_print_enable_status() 218 | if self._enable_print_current_row: 219 | self._print_column(var_value) 220 | 221 | if self._cur_col_index == len(self.config.columns) - 1: 222 | self.row_index += 1 223 | self._cur_col_index = 0 224 | else: 225 | self._cur_col_index += 1 226 | 227 | def endline(self, msg=""): 228 | self._update_print_enable_status() 229 | if self._enable_print_current_row: 230 | self._print(msg) 231 | self._println() 232 | self.row_index += 1 233 | self._cur_col_index = 0 234 | 235 | def elapsed_since_init(self, round_seconds=True): 236 | elapsed = datetime.datetime.now() - self.init_time 237 | if round_seconds: 238 | rounded_seconds = round(elapsed.total_seconds()) 239 | elapsed = datetime.timedelta(seconds=rounded_seconds) 240 | return elapsed 241 | 242 | def force_print_next_row(self): 243 | self._force_print_row_index = self.row_index 244 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="scolp", 8 | version="0.1.4", 9 | author="David Ohana", 10 | author_email="davidoha@gmail.com", 11 | description="Streaming Column Printer", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/davidohana/python-scolp", 15 | packages=setuptools.find_packages(), 16 | py_modules=['scolp'], 17 | classifiers=[ 18 | "Programming Language :: Python :: 3.6", 19 | "Programming Language :: Python :: 3.7", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Topic :: Software Development :: Libraries", 23 | ], 24 | ) 25 | --------------------------------------------------------------------------------