linerepeat 0.1.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.
linerepeat/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from linerepeat.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
linerepeat/cli.py ADDED
@@ -0,0 +1,84 @@
1
+ from pathlib import Path
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from linerepeat.repeat import *
7
+ from linerepeat.stream import *
8
+
9
+ app = typer.Typer(add_completion=False)
10
+
11
+
12
+ @app.command()
13
+ def linerepeat(
14
+ count: Annotated[
15
+ int,
16
+ typer.Argument(
17
+ min=1,
18
+ help="How many times to repeat.",
19
+ ),
20
+ ],
21
+ input_file: Annotated[
22
+ Path | None,
23
+ typer.Option(
24
+ "-i",
25
+ "--input",
26
+ exists=True,
27
+ dir_okay=False,
28
+ readable=True,
29
+ writable=True,
30
+ help="Edit this file in place instead of reading stdin.",
31
+ ),
32
+ ] = None,
33
+ output_file: Annotated[
34
+ Path | None,
35
+ typer.Option(
36
+ "-o",
37
+ "--output",
38
+ dir_okay=False,
39
+ help="Write here instead of stdout. Can't be used with --input.",
40
+ ),
41
+ ] = None,
42
+ separator: Annotated[
43
+ str,
44
+ typer.Option(
45
+ "-s",
46
+ "--separator",
47
+ help="Line placed between repeats.",
48
+ ),
49
+ ] = "",
50
+ mode: Annotated[
51
+ Mode,
52
+ typer.Option(
53
+ "-m",
54
+ "--mode",
55
+ help="Repeat single lines or blocks.",
56
+ ),
57
+ ] = Mode.LINE,
58
+ till: Annotated[
59
+ int,
60
+ typer.Option(
61
+ "-t",
62
+ "--till",
63
+ help="Lines per block (block mode).",
64
+ ),
65
+ ] = 0,
66
+ ):
67
+
68
+ if input_file is not None and output_file is not None:
69
+ raise typer.BadParameter(
70
+ "can't be used with --input, which edits the file in place.",
71
+ param_hint="--output",
72
+ )
73
+
74
+ # Open input (stdin or file) and process with repeat()
75
+ with open_input(input_file) as src:
76
+ try:
77
+ result = repeat(src, count, separator=separator, mode=mode, till=till)
78
+ except ValueError as e:
79
+ raise typer.BadParameter(str(e)) from e
80
+
81
+ # Write output to stdout, file, or back to input file
82
+ with open_output(input_path=input_file, output_path=output_file) as out:
83
+ write_lines(out, result)
84
+
linerepeat/repeat.py ADDED
@@ -0,0 +1,70 @@
1
+ from collections.abc import Iterable, Iterator
2
+ from enum import StrEnum
3
+ from itertools import islice
4
+ from itertools import repeat as _times
5
+
6
+
7
+ class Mode(StrEnum):
8
+ LINE = "line"
9
+ BLOCK = "block"
10
+
11
+
12
+ def _clean(lines: Iterable[str]) -> Iterator[str]:
13
+ """Strip trailing newlines and skip empty lines."""
14
+ for line in lines:
15
+ s = line.rstrip("\n\r")
16
+ if s.strip():
17
+ yield s
18
+
19
+
20
+ def _chunks(items: Iterator[str], size: int) -> Iterator[list[str]]:
21
+ """
22
+ Group items into chunks of the given size.
23
+
24
+ Args:
25
+ items: Iterator of items to chunk.
26
+ size: Number of items per chunk.
27
+
28
+ Yields:
29
+ Lists of items, each up to 'size' long.
30
+ """
31
+ while chunk := list(islice(items, size)):
32
+ yield chunk
33
+
34
+
35
+ def _generate(lines, count, separator, mode, till) -> Iterator[str]:
36
+ """
37
+ In LINE mode: each line is repeated 'count' times, with separator between.
38
+ In BLOCK mode: lines are grouped into blocks of 'till' lines, then each
39
+ block is repeated 'count' times with separator between blocks.
40
+ """
41
+ first = True
42
+ if mode == Mode.LINE:
43
+ for line in lines:
44
+ if not first:
45
+ yield separator
46
+ first = False
47
+ yield from _times(line, count)
48
+ else:
49
+ for block in _chunks(lines, till):
50
+ for _ in range(count):
51
+ if not first:
52
+ yield separator
53
+ first = False
54
+ yield from block
55
+
56
+
57
+ def repeat(
58
+ lines: Iterable[str],
59
+ count: int,
60
+ *,
61
+ separator: str = "",
62
+ mode: Mode = Mode.LINE,
63
+ till: int = 0,
64
+ ) -> Iterator[str]:
65
+
66
+ if count < 1:
67
+ raise ValueError("count must be at least 1.")
68
+ if mode == Mode.BLOCK and till < 2:
69
+ raise ValueError("till must be at least 2 in block mode.")
70
+ return _generate(_clean(lines), count, separator, mode, till)
linerepeat/stream.py ADDED
@@ -0,0 +1,87 @@
1
+ import os
2
+ import shutil
3
+ import sys
4
+ import tempfile
5
+ from collections.abc import Generator, Iterable
6
+ from contextlib import contextmanager
7
+ from pathlib import Path
8
+ from typing import TextIO
9
+
10
+
11
+ @contextmanager
12
+ def open_input(path: Path | None) -> Generator[TextIO]:
13
+ """Open an input file or stdin."""
14
+ if path is None:
15
+ yield sys.stdin
16
+ else:
17
+ with path.open(encoding="utf-8") as f:
18
+ yield f
19
+
20
+
21
+ @contextmanager
22
+ def open_output(
23
+ *,
24
+ input_path: Path | None,
25
+ output_path: Path | None,
26
+ ) -> Generator[TextIO]:
27
+ """
28
+ Open an output destination based on arguments.
29
+
30
+ Priority:
31
+ 1. If input_path is given: edit that file in place.
32
+ 2. Else if output_path is given: write to that file.
33
+ 3. Else: write to stdout.
34
+
35
+ Yields:
36
+ Open file handle for writing.
37
+ """
38
+
39
+ if input_path is not None:
40
+ with _replace_in_place(input_path) as f:
41
+ yield f
42
+ elif output_path is not None:
43
+ with output_path.open("w", encoding="utf-8") as f:
44
+ yield f
45
+ else:
46
+ yield sys.stdout
47
+
48
+
49
+ @contextmanager
50
+ def _replace_in_place(path: Path) -> Generator[TextIO]:
51
+ """
52
+ Replace a file by writing to a temp file first then swap it.
53
+
54
+ Args:
55
+ path: File to replace.
56
+
57
+ Yields:
58
+ Open temp file handle for writing.
59
+ """
60
+ target = path.resolve()
61
+ fd, tmp_name = tempfile.mkstemp(
62
+ dir=target.parent,
63
+ prefix=f".{target.name}.",
64
+ suffix=".tmp",
65
+ )
66
+
67
+ tmp = Path(tmp_name)
68
+ try:
69
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
70
+ yield f
71
+ shutil.copymode(target, tmp)
72
+ os.replace(tmp, target)
73
+ except BaseException:
74
+ tmp.unlink(missing_ok=True) # Clean up temp file on any error
75
+ raise
76
+
77
+
78
+ def write_lines(out: TextIO, lines: Iterable[str]) -> None:
79
+ """
80
+ Write lines to an output stream, adding newline after each.
81
+
82
+ Args:
83
+ out: Output file handle.
84
+ lines: Lines to write.
85
+ """
86
+ out.writelines(f"{line}\n" for line in lines)
87
+
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: linerepeat
3
+ Version: 0.1.0
4
+ Summary: A small CLI tool for repeating lines
5
+ Keywords: cli,text,lines,repeat,typer
6
+ Author: Abdullah Alhamdi
7
+ Author-email: Abdullah Alhamdi <trymemain.pb.hug@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Environment :: Console
16
+ Requires-Dist: typer>=0.27.3
17
+ Requires-Python: >=3.14
18
+ Project-URL: Repository, https://github.com/cqveman/linerepeat
19
+ Project-URL: Issues, https://github.com/cqveman/linerepeat/issues
20
+ Description-Content-Type: text/markdown
21
+
22
+ # linerepeat
23
+
24
+ A small CLI tool for repeating lines from stdin or files. Useful for generating test data, creating separators, duplicating content, or any task where you need to repeat text patterns.
25
+
26
+ **Features:**
27
+ - **Line mode** (default): Repeat each input line N times
28
+ - **Block mode**: Repeat groups of lines as blocks
29
+ - **Custom separators**: Add delimiters between input line groups
30
+ - **Flexible I/O**: Read from stdin/files, write to stdout/files
31
+
32
+ ## Install
33
+
34
+ ### From PyPI
35
+ ```bash
36
+ pip install linerepeat
37
+ ```
38
+
39
+ ### Pre-built Binaries
40
+ Download the latest `linerepeat` binary (Linux) or `linerepeat.exe` (Windows) from [GitHub Releases](https://github.com/cqveman/linerepeat/releases).
41
+
42
+ ## Usage
43
+
44
+ ```console
45
+ $ linerepeat [OPTIONS] {count}
46
+ ```
47
+
48
+ **Arguments:**
49
+
50
+ * `count`: How many times to repeat. [required]
51
+
52
+ **Options:**
53
+
54
+ * `-i, --input <file>`: Edit this file in place instead of reading stdin.
55
+ * `-o, --output <file>`: Write here instead of stdout. Can't be used with --input.
56
+ * `-s, --separator <str>`: Line placed between repeats.
57
+ * `-m, --mode <line|block>`: Repeat single lines or blocks. [default: line]
58
+ * `-t, --till <int>`: Lines per block (block mode). [default: 0]
59
+ * `--help`: Show this message and exit.
60
+
61
+ ## Examples
62
+
63
+ ```bash
64
+ # Repeat each line 3 times (single line - no separator printed)
65
+ echo "hello" | linerepeat 3
66
+ # hello
67
+ # hello
68
+ # hello
69
+
70
+ # With multiple input lines, separator goes BETWEEN lines
71
+ echo -e "a\nb" | linerepeat 2 -s "---"
72
+ # a
73
+ # a
74
+ # ---
75
+ # b
76
+ # b
77
+
78
+ # Block mode with separator between blocks
79
+ echo -e "line1\nline2\nline3\nline4" | linerepeat 2 -m block -t 2 -s "==="
80
+ # line1
81
+ # line2
82
+ # ===
83
+ # line1
84
+ # line2
85
+ # ===
86
+ # line3
87
+ # line4
88
+ # ===
89
+ # line3
90
+ # line4
91
+
92
+ # In-place: duplicate every line in a file
93
+ linerepeat 2 -i file.txt
94
+ ```
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,9 @@
1
+ linerepeat/__init__.py,sha256=qFBngyNSMJmQkLqHmg_7Qdfsj1luWFrnoqTNAQjcnS4,69
2
+ linerepeat/cli.py,sha256=Wj9TrbphG4Cn9fSLmeAoz4JpffF_rEttJpEztdEfwhA,2108
3
+ linerepeat/repeat.py,sha256=a0MhnC4mICYXc1AF33Tupi-6XokHikWaHAWeIBZIprc,1887
4
+ linerepeat/stream.py,sha256=jXIkMLoY7y_u-DwDCWoCTOMk0ED9PaDa0ErEInBI4W4,2041
5
+ linerepeat-0.1.0.dist-info/licenses/LICENSE,sha256=CnFMEubZ7OsHq09PjFlPXpUK1a0XoWxmpvytO_38FCI,1068
6
+ linerepeat-0.1.0.dist-info/WHEEL,sha256=1dD_GoEqEIxQ7VnrUExgdj_eExY4b4UgfR0iBlGKVac,80
7
+ linerepeat-0.1.0.dist-info/entry_points.txt,sha256=hV4ekhXNleapYp0Sl2JbfDCLRTCIfYu_5GtNvFbW0S8,51
8
+ linerepeat-0.1.0.dist-info/METADATA,sha256=qpZi9j5Km-Mx-eohcNr6rPDskBiLhDIakf2Hf5UEv8k,2515
9
+ linerepeat-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.13.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ linerepeat = linerepeat.cli:app
3
+
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Abdullah Alhamdi
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.