tagsnip 1.0.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rostislav Brož
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.
tagsnip-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: tagsnip
3
+ Version: 1.0.0
4
+ Summary: Python package for extracting tagged code snippets from local files or remote sources.
5
+ Author: Rostislav Brož
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/brozrost/tagsnip
8
+ Project-URL: Repository, https://github.com/brozrost/tagsnip
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Requires-Dist: requests
13
+ Dynamic: license-file
14
+
15
+ `tagsnip` is a Python CLI tool for extracting tagged code snippets from local files or remote URLs, such as GitHub raw files. It serves as the backend for the tagsnip LaTeX package and enables dynamic inclusion of code snippets directly in LaTeX documents.
16
+
17
+ <div align="center">
18
+ <a href="https://github.com/brozrost/tagsnip/actions">
19
+ <img src="https://github.com/brozrost/tagsnip/actions/workflows/python-package.yml/badge.svg">
20
+ </a>
21
+ <a href="https://github.com/brozrost/tagsnip/graphs/contributors">
22
+ <img src="https://img.shields.io/github/contributors/brozrost/tagsnip">
23
+ </a>
24
+ <a href="https://github.com/brozrost/tagsnip/issues">
25
+ <img src="https://img.shields.io/github/issues/brozrost/tagsnip">
26
+ </a>
27
+ <a href="https://github.com/brozrost/tagsnip/pulls">
28
+ <img src="https://img.shields.io/github/issues-pr/brozrost/tagsnip">
29
+ </a>
30
+ </div>
31
+
32
+ ## Installation
33
+
34
+ Install `tagsnip` from PyPI:
35
+
36
+ ```sh
37
+ pip install tagsnip
38
+ ```
39
+
40
+ After installation, the command `tagsnip` should be available in your shell:
41
+
42
+ ```sh
43
+ tagsnip --help
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```sh
49
+ tagsnip -s <source> -t <tag> [-o <output>]
50
+ ```
51
+
52
+ ```sh
53
+ tagsnip -c <output>
54
+ ```
55
+
56
+ ### Command-line options
57
+ ```text
58
+ -s, --source Local file path or remote URL.
59
+ -t, --tag Snippet tag to extract.
60
+ -o, --output Optional output file path.
61
+ -c, --cleanup Removes temporary files at specified path.
62
+ ```
63
+
64
+ `--source` and `--tag` are mandatory arguments. If `--output` is provided, `tagsnip` writes the extracted snippet to the selected file. Otherwise, the snippet is written to `stdout`.
65
+
66
+ When `--output` is used, `tagsnip` also writes a metadata file next to the output file. For example, if the output file is `snippet.tmp`, then the metadata file is `snippet.tmp.meta`. Both files can be removed using the `--cleanup` option.
67
+
68
+ The metadata file contains the original start and end line numbers of the extracted snippet. This is used by the LaTeX package to preserve source line numbering.
69
+
70
+ ### Supported sources
71
+
72
+ - Local text files
73
+ - Remote text files available over HTTP or HTTPS
74
+ - GitHub raw file URLs
75
+
76
+ ### Tag format
77
+
78
+ `tagsnip` extracts code between two matching markers:
79
+
80
+ ```text
81
+ tagsnip-start <tag>
82
+ ...
83
+ tagsnip-end <tag>
84
+ ```
85
+
86
+ Tags are case-sensitive. Markers must appear on separate lines. The tag must follow the marker name after one space.
87
+
88
+ For example:
89
+
90
+ ```python
91
+ # tagsnip-start demo
92
+ print("This line will be extracted.")
93
+ # tagsnip-end demo
94
+ ```
95
+
96
+ The marker lines themselves are not included in the extracted snippet.
97
+
98
+ ### Error handling
99
+
100
+ `tagsnip` reports errors when:
101
+
102
+ - The tag is not found
103
+ - Start/end markers are mismatched
104
+ - Multiple start markers exist
105
+ - The source cannot be fetched
106
+
107
+ ## Quick example
108
+
109
+ ### Example source file
110
+
111
+ ```python
112
+ # tagsnip-start demo
113
+ def main():
114
+ x = 1
115
+ y = 2
116
+ print(x + y)
117
+
118
+ return 0
119
+ # tagsnip-end demo
120
+
121
+ if __name__ == "__main__":
122
+ import sys
123
+ sys.exit(main())
124
+ ```
125
+
126
+ ### CLI usage
127
+
128
+ Write the snippet to `stdout`:
129
+
130
+ ```bash
131
+ tagsnip -s example.py -t demo
132
+ ```
133
+
134
+ Write the snippet to a file:
135
+ ```bash
136
+ tagsnip -s example.py -t demo -o out/out.txt
137
+ ```
138
+
139
+ Get snippet from URL:
140
+ ```bash
141
+ tagsnip -s https://raw.githubusercontent.com/brozrost/tagsnip/main/docs/example.py -t demo
142
+ ```
143
+
144
+ Remove temporary files:
145
+ ```bash
146
+ tagsnip -c out/out.txt
147
+ ```
148
+
149
+ ### Python usage
150
+
151
+ ```python
152
+ from tagsnip import extractor
153
+
154
+ snippet = extractor.extract_from_file("example.py", "demo")
155
+ ```
156
+
157
+ ## Compatibility note
158
+
159
+ The Python package and the LaTeX package should be kept in sync. Backwards compatibility is not guaranteed.
@@ -0,0 +1,145 @@
1
+ `tagsnip` is a Python CLI tool for extracting tagged code snippets from local files or remote URLs, such as GitHub raw files. It serves as the backend for the tagsnip LaTeX package and enables dynamic inclusion of code snippets directly in LaTeX documents.
2
+
3
+ <div align="center">
4
+ <a href="https://github.com/brozrost/tagsnip/actions">
5
+ <img src="https://github.com/brozrost/tagsnip/actions/workflows/python-package.yml/badge.svg">
6
+ </a>
7
+ <a href="https://github.com/brozrost/tagsnip/graphs/contributors">
8
+ <img src="https://img.shields.io/github/contributors/brozrost/tagsnip">
9
+ </a>
10
+ <a href="https://github.com/brozrost/tagsnip/issues">
11
+ <img src="https://img.shields.io/github/issues/brozrost/tagsnip">
12
+ </a>
13
+ <a href="https://github.com/brozrost/tagsnip/pulls">
14
+ <img src="https://img.shields.io/github/issues-pr/brozrost/tagsnip">
15
+ </a>
16
+ </div>
17
+
18
+ ## Installation
19
+
20
+ Install `tagsnip` from PyPI:
21
+
22
+ ```sh
23
+ pip install tagsnip
24
+ ```
25
+
26
+ After installation, the command `tagsnip` should be available in your shell:
27
+
28
+ ```sh
29
+ tagsnip --help
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```sh
35
+ tagsnip -s <source> -t <tag> [-o <output>]
36
+ ```
37
+
38
+ ```sh
39
+ tagsnip -c <output>
40
+ ```
41
+
42
+ ### Command-line options
43
+ ```text
44
+ -s, --source Local file path or remote URL.
45
+ -t, --tag Snippet tag to extract.
46
+ -o, --output Optional output file path.
47
+ -c, --cleanup Removes temporary files at specified path.
48
+ ```
49
+
50
+ `--source` and `--tag` are mandatory arguments. If `--output` is provided, `tagsnip` writes the extracted snippet to the selected file. Otherwise, the snippet is written to `stdout`.
51
+
52
+ When `--output` is used, `tagsnip` also writes a metadata file next to the output file. For example, if the output file is `snippet.tmp`, then the metadata file is `snippet.tmp.meta`. Both files can be removed using the `--cleanup` option.
53
+
54
+ The metadata file contains the original start and end line numbers of the extracted snippet. This is used by the LaTeX package to preserve source line numbering.
55
+
56
+ ### Supported sources
57
+
58
+ - Local text files
59
+ - Remote text files available over HTTP or HTTPS
60
+ - GitHub raw file URLs
61
+
62
+ ### Tag format
63
+
64
+ `tagsnip` extracts code between two matching markers:
65
+
66
+ ```text
67
+ tagsnip-start <tag>
68
+ ...
69
+ tagsnip-end <tag>
70
+ ```
71
+
72
+ Tags are case-sensitive. Markers must appear on separate lines. The tag must follow the marker name after one space.
73
+
74
+ For example:
75
+
76
+ ```python
77
+ # tagsnip-start demo
78
+ print("This line will be extracted.")
79
+ # tagsnip-end demo
80
+ ```
81
+
82
+ The marker lines themselves are not included in the extracted snippet.
83
+
84
+ ### Error handling
85
+
86
+ `tagsnip` reports errors when:
87
+
88
+ - The tag is not found
89
+ - Start/end markers are mismatched
90
+ - Multiple start markers exist
91
+ - The source cannot be fetched
92
+
93
+ ## Quick example
94
+
95
+ ### Example source file
96
+
97
+ ```python
98
+ # tagsnip-start demo
99
+ def main():
100
+ x = 1
101
+ y = 2
102
+ print(x + y)
103
+
104
+ return 0
105
+ # tagsnip-end demo
106
+
107
+ if __name__ == "__main__":
108
+ import sys
109
+ sys.exit(main())
110
+ ```
111
+
112
+ ### CLI usage
113
+
114
+ Write the snippet to `stdout`:
115
+
116
+ ```bash
117
+ tagsnip -s example.py -t demo
118
+ ```
119
+
120
+ Write the snippet to a file:
121
+ ```bash
122
+ tagsnip -s example.py -t demo -o out/out.txt
123
+ ```
124
+
125
+ Get snippet from URL:
126
+ ```bash
127
+ tagsnip -s https://raw.githubusercontent.com/brozrost/tagsnip/main/docs/example.py -t demo
128
+ ```
129
+
130
+ Remove temporary files:
131
+ ```bash
132
+ tagsnip -c out/out.txt
133
+ ```
134
+
135
+ ### Python usage
136
+
137
+ ```python
138
+ from tagsnip import extractor
139
+
140
+ snippet = extractor.extract_from_file("example.py", "demo")
141
+ ```
142
+
143
+ ## Compatibility note
144
+
145
+ The Python package and the LaTeX package should be kept in sync. Backwards compatibility is not guaranteed.
@@ -0,0 +1,187 @@
1
+ ### `tagsnip` is a LaTeX package for including tagged code snippets from local files or remote URLs. It extracts marked sections of code and typesets them in a consistent style, supporting reproducible and maintainable documentation.
2
+
3
+ <div align="center">
4
+ <a href="https://github.com/brozrost/tagsnip/actions">
5
+ <img src="https://github.com/brozrost/tagsnip/actions/workflows/python-package.yml/badge.svg">
6
+ </a>
7
+ <a href="https://github.com/brozrost/tagsnip/graphs/contributors">
8
+ <img src="https://img.shields.io/github/contributors/brozrost/tagsnip">
9
+ </a>
10
+ <a href="https://github.com/brozrost/tagsnip/issues">
11
+ <img src="https://img.shields.io/github/issues/brozrost/tagsnip">
12
+ </a>
13
+ <a href="https://github.com/brozrost/tagsnip/pulls">
14
+ <img src="https://img.shields.io/github/issues-pr/brozrost/tagsnip">
15
+ </a>
16
+ </div>
17
+
18
+ **Documentation:** <a href="https://github.com/brozrost/tagsnip/blob/main/docs/tagsnip-docs.pdf">tagsnip-docs.pdf</a>
19
+ **Czech documentation:** <a href="https://github.com/brozrost/tagsnip/blob/main/docs/tagsnip-docs-czech.pdf">tagsnip-docs-czech.pdf</a>
20
+
21
+ > The documentation files also double as example documents for `tagsnip`.
22
+
23
+ ## Installation
24
+
25
+ `tagsnip` consists of two parts:
26
+
27
+ 1. LaTeX package `tagsnip.sty`,
28
+ 2. Python backend package `tagsnip`.
29
+
30
+ Both parts are required.
31
+
32
+ ### Installing the LaTeX package from CTAN
33
+
34
+ Download the <a href="https://ctan.org/pkg/tagsnip">`tagsnip` package archive from CTAN</a> and extract it. For a local project installation, copy `tagsnip.sty` next to your main `.tex` file:
35
+
36
+ ```sh
37
+ project/
38
+ ├── main.tex
39
+ └── tagsnip.sty
40
+ ```
41
+
42
+ This is the simplest installation method and is sufficient for compiling a single project. For a user-wide installation, place `tagsnip.sty` into your local TeX tree.
43
+
44
+ Also see: https://ctan.org/pkg/tagsnip
45
+
46
+ ### Installing the Python backend
47
+
48
+ `tagsnip` uses a Python backend with the same name to parse source files. The backend is published on PyPI. Before using the LaTeX package, install the backend:
49
+
50
+ ```sh
51
+ pip install tagsnip
52
+ ```
53
+
54
+ Also see: https://pypi.org/project/tagsnip/
55
+
56
+ After installation, the backend must be available in the system `PATH` under the command name `tagsnip`. You can check this with:
57
+
58
+ ~~~sh
59
+ tagsnip --help
60
+ ~~~
61
+
62
+ ## Compilation
63
+
64
+ ```sh
65
+ lualatex --shell-escape docs/tagsnip-docs.tex
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ **Example document:** <a href="https://github.com/brozrost/tagsnip/blob/main/docs/docs.pdf">docs.pdf</a>
71
+
72
+ `tagsnip` defines the command `\IncludeCode`, which is used as follows:
73
+
74
+ ```tex
75
+ \IncludeCode[options]{source}{tag}{language}{caption}
76
+ ```
77
+
78
+ The optional argument `options` allows the user to override code formatting settings. These options are passed directly to `minted`, so they must be valid `minted` options and must be separated by commas. For example:
79
+
80
+ ```tex
81
+ \IncludeCode[
82
+ firstnumber=1,
83
+ fontsize=\scriptsize,
84
+ style=monokai
85
+ ]{docs/example.py}{tag1}{Python}{Example snippet.}
86
+ ```
87
+
88
+ The option `firstnumber=1` starts line numbering at line 1, `fontsize=\scriptsize` changes the font size, and `style=monokai` changes the syntax highlighting style.
89
+
90
+ Without changing `firstnumber`, `tagsnip` preserves the original line numbers from the source file.
91
+
92
+ The mandatory argument `source` defines where the code should be loaded from. It can be either a local file path or a URL pointing to a remote text file. `tagsnip` distinguishes these cases automatically.
93
+
94
+ The mandatory argument `tag` specifies the unique keyword identifying the requested snippet. A snippet is delimited in the source file by `tagsnip-start` and `tagsnip-end` markers:
95
+
96
+ ```python
97
+ # tagsnip-start tag1
98
+ def main():
99
+ x = 1
100
+ y = 2
101
+ print(x + y)
102
+
103
+ return 0
104
+ # tagsnip-end tag1
105
+ ```
106
+
107
+ The tag must follow the marker after exactly one space.
108
+
109
+ The mandatory argument `language` selects the programming language used for syntax highlighting.
110
+
111
+ The final argument `caption` defines the snippet caption. It may be empty, but the braces must still be written.
112
+
113
+ ## Local snippet
114
+
115
+ Suppose the local file `docs/example.py` contains a function `main()` marked with the tag `tag1`, as seen above. The function can be included in the document with:
116
+
117
+ ```tex
118
+ \IncludeCode{example.py}{tag1}{Python}{Úryvek 2: ...}
119
+ ```
120
+ <div align="center">
121
+ <img width="835" height="265" alt="# tagsnip-start tag" src="https://github.com/user-attachments/assets/4ca1bd1a-abe8-4478-963d-15c42fbe9479" />
122
+ </div>
123
+
124
+
125
+ ## Remote snippet
126
+
127
+ `tagsnip` can also include snippets from source files available through web URLs.
128
+
129
+ For example, a snippet marked with the tag `tag2` in a remote Python file can be included as follows:
130
+
131
+ ```tex
132
+ \IncludeCode[firstnumber=1]{https://raw.githubusercontent.com/brozrost/tagsnip/main/docs/example2.py}{tag2}{Python}{Code snippet from a remote file.}
133
+ ```
134
+
135
+ <div align="center">
136
+ <img width="833" height="324" alt="Pasted Graphic 1 2" src="https://github.com/user-attachments/assets/02dfe4b5-0340-42b1-bb25-63cc0b331827" />
137
+ </div>
138
+
139
+ The remote file must be accessible over HTTP or HTTPS and must be readable as a plain text source file.
140
+
141
+ ## Architecture
142
+
143
+ `tagsnip` consists of two main parts: a LaTeX frontend package and an external backend utility written in Python.
144
+
145
+ The frontend defines the command `\IncludeCode` and handles the final typesetting of extracted snippets through the `minted` package.
146
+
147
+ The backend is responsible for accessing source files, finding marked code regions, and extracting them into temporary files.
148
+
149
+ During document compilation, the frontend uses shell escape to call the backend utility. It passes the source path or URL and the requested tag to the backend. The backend writes the extracted code to a temporary file, which is then inserted into the document and typeset by `minted`.
150
+
151
+ ## Backend
152
+
153
+ The Python package `tagsnip` provides a command-line interface that accepts a source file, a tag name, and an output file path.
154
+
155
+ The backend loads the source file, searches for the corresponding pair of `tagsnip-start` and `tagsnip-end` markers, and extracts the text between them.
156
+
157
+ The package also checks for error states, such as:
158
+
159
+ - a non-existent source file,
160
+ - an inaccessible remote file,
161
+ - a missing tag,
162
+ - a missing start or end marker,
163
+ - multiple occurrences of the same tag in one source file.
164
+
165
+ If an error is detected, the backend raises an exception. This interrupts the LaTeX compilation and prints an appropriate error message.
166
+
167
+ For local files, the backend uses Python's `pathlib` module. For tag matching, it uses `re`. For loading remote files over HTTP, it uses the `requests` library.
168
+
169
+ ## Limitations
170
+
171
+ - `tagsnip` requires LuaLaTeX.
172
+ - The document must be compiled with shell escape enabled, for example with `--shell-escape`.
173
+ - The Python backend must be installed and available in the system `PATH` under the command `tagsnip`.
174
+ - `tagsnip` depends on `minted`.
175
+ - Remote source files must be accessible over HTTP or HTTPS.
176
+ - Remote source files must be plain text files.
177
+
178
+ ## Security note
179
+
180
+ `tagsnip` requires shell escape because it calls an external Python backend during compilation and removes temporary files (lines 80, 93, and 94 in `tagsnip.sty`).
181
+
182
+ Only compile trusted documents with shell escape enabled.
183
+
184
+ ---
185
+ Copyright (c) 2026 Rostislav Brož.
186
+
187
+ > `tagsnip` is distributed under the MIT License. The full license text is included in the repository in the [`LICENSE`](https://github.com/brozrost/tagsnip/blob/main/LICENSE.md) file.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tagsnip"
7
+ version = "1.0.0"
8
+ description = "Python package for extracting tagged code snippets from local files or remote sources."
9
+ readme = "README-pypi.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["requests"]
12
+ authors = [
13
+ { name = "Rostislav Brož" }
14
+ ]
15
+ license = "MIT"
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/brozrost/tagsnip"
19
+ Repository = "https://github.com/brozrost/tagsnip"
20
+
21
+ [tool.setuptools]
22
+ packages = ["tagsnip"]
23
+
24
+ [project.scripts]
25
+ tagsnip = "tagsnip.cli:main"
@@ -0,0 +1,8 @@
1
+ [options.entry_points]
2
+ console_scripts =
3
+ tagsnip = tagsnip.cli:main
4
+
5
+ [egg_info]
6
+ tag_build =
7
+ tag_date = 0
8
+
File without changes
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Entrypoint for `python -m tagsnip`
4
+ """
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,113 @@
1
+ import sys
2
+ import argparse
3
+ from pathlib import Path
4
+
5
+ from tagsnip import extractor
6
+ from tagsnip import fetcher
7
+ from tagsnip import validate
8
+
9
+ def cleanup_generated_files(output_path: str) -> None:
10
+ snippet_path = Path(output_path)
11
+ meta_path = Path(str(snippet_path) + ".meta")
12
+
13
+ for path in (snippet_path, meta_path):
14
+ try:
15
+ path.unlink()
16
+ except FileNotFoundError:
17
+ pass
18
+ except OSError as exc:
19
+ raise RuntimeError(f"Error: Could not remove temporary file: {path}") from exc
20
+
21
+ def main() -> int:
22
+ parser = argparse.ArgumentParser(
23
+ description="tagsnip is a Python package for extracting tagged code snippets " \
24
+ "from local files or remote sources.",
25
+ usage="""
26
+ tagsnip --help
27
+
28
+ tagsnip --source <file path> --tag <tag>
29
+ tagsnip -s example.py -t demo
30
+
31
+ tagsnip --source <file path> --tag <tag> --out <out file path>
32
+ tagsnip -s example.py -t demo -o out/out.txt
33
+
34
+ tagsnip --source <url> --tag <tag>
35
+ tagsnip -s https://raw.githubusercontent.com/brozrost/tagsnip/main/docs/example.py -t demo
36
+
37
+ tagsnip --cleanup <generated file path>
38
+ tagsnip --cleanup out/out.txt
39
+ """
40
+ )
41
+
42
+ parser.add_argument(
43
+ "-s", "--source",
44
+ help="Path to a local file or URL"
45
+ )
46
+ parser.add_argument(
47
+ "-t", "--tag",
48
+ help="Snippet tag name."
49
+ )
50
+ parser.add_argument(
51
+ "-o", "--out",
52
+ help="Path to the output file. If omitted, prints to stdout."
53
+ )
54
+
55
+ parser.add_argument(
56
+ "-c", "--cleanup",
57
+ metavar="FILE",
58
+ help="Remove a generated snippet file and its .meta file."
59
+ )
60
+
61
+ args = parser.parse_args()
62
+
63
+ if args.cleanup:
64
+ try:
65
+ cleanup_generated_files(args.cleanup)
66
+ except RuntimeError as exc:
67
+ print(f"Error: {exc}", file=sys.stderr)
68
+ return 1
69
+
70
+ return 0
71
+
72
+ if not args.source or not args.tag:
73
+ parser.error("Error: Arguments -s/--source and -t/--tag are required unless --cleanup is used.")
74
+
75
+
76
+ try:
77
+ if validate.is_url(args.source):
78
+ fetch = fetcher.FetcherClient()
79
+ text = fetch.fetch_text(args.source)
80
+ else:
81
+ text = Path(args.source).read_text(encoding="utf-8")
82
+
83
+ snippet, first_line_num, last_line_num = extractor.extract_tagged_block(text, args.tag)
84
+
85
+ except fetcher.FetcherError as exc:
86
+ print(f"Error: {exc}", file=sys.stderr)
87
+ return 1
88
+ except OSError:
89
+ print(f"Error: Could not read file: {args.source}", file=sys.stderr)
90
+ return 1
91
+ except extractor.TagsnipError as exc:
92
+ print(f"Error: {exc}", file=sys.stderr)
93
+ return 1
94
+
95
+ if args.out:
96
+ output_path = Path(args.out)
97
+
98
+ try:
99
+ output_path.parent.mkdir(parents=True, exist_ok=True)
100
+ output_path.write_text(snippet, encoding="utf-8")
101
+
102
+ meta_path = Path(str(output_path) + ".meta")
103
+ meta_path.write_text(f"{first_line_num}\n{last_line_num}", encoding="utf-8")
104
+ except OSError as exc:
105
+ print(f"Error: Could not write file: {output_path}")
106
+ return 1
107
+ else:
108
+ print(snippet)
109
+
110
+ return 0
111
+
112
+ if __name__ == "__main__":
113
+ sys.exit(main())
@@ -0,0 +1,51 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ class TagsnipError(RuntimeError):
5
+ pass
6
+
7
+ def marker_matches(line: str, marker: str) -> bool:
8
+ pattern = rf"{re.escape(marker)}(?!\S)"
9
+ return re.search(pattern, line) is not None
10
+
11
+ def extract_tagged_block(text: str, tag: str):
12
+ start_marker = f"tagsnip-start {tag}"
13
+ end_marker = f"tagsnip-end {tag}"
14
+
15
+ lines = text.splitlines()
16
+
17
+ start_index = None
18
+ end_index = None
19
+
20
+ for i, line in enumerate(lines):
21
+ if marker_matches(line, start_marker):
22
+ if start_index is not None:
23
+ raise TagsnipError(f"Multiple start tags found for '{tag}'")
24
+
25
+ start_index = i
26
+
27
+ if start_index is None:
28
+ raise TagsnipError(f"Start tag not found for '{tag}'")
29
+
30
+ for i in range(start_index + 1, len(lines)):
31
+ if marker_matches(lines[i], end_marker):
32
+ end_index = i
33
+ break
34
+
35
+ if end_index is None:
36
+ raise TagsnipError(f"End tag not found for '{tag}'")
37
+
38
+ return "\n".join(lines[start_index + 1:end_index]), start_index + 2, end_index
39
+
40
+ def extract_from_file(path: str | Path, tag: str) -> str:
41
+ file_path = Path(path)
42
+
43
+ if not file_path.is_file():
44
+ raise TagsnipError(f"File not found: {file_path}")
45
+
46
+ try:
47
+ text = file_path.read_text(encoding="utf-8")
48
+ except OSError as exc:
49
+ raise TagsnipError(f"Could not read file: {file_path}") from exc
50
+
51
+ return extract_tagged_block(text, tag)
@@ -0,0 +1,22 @@
1
+ import requests
2
+
3
+ class FetcherError(RuntimeError):
4
+ pass
5
+
6
+ class FetcherClient:
7
+ def __init__(self, timeout: float = 30.0):
8
+ self.timeout = timeout
9
+
10
+ def fetch_text(self, url: str) -> str:
11
+ try:
12
+ response = requests.get(url, timeout=self.timeout)
13
+ response.raise_for_status()
14
+ except requests.HTTPError as exc:
15
+ raise FetcherError(f"HTTP error while fetching '{url}': {exc}") from exc
16
+ except requests.RequestException as exc:
17
+ raise FetcherError(f"Network error while fetching '{url}': {exc}") from exc
18
+
19
+ if not response.text.strip():
20
+ raise FetcherError(f"Empty response from {url}")
21
+
22
+ return response.text
@@ -0,0 +1,5 @@
1
+ from urllib.parse import urlparse
2
+
3
+ def is_url(source: str) -> bool:
4
+ parsed = urlparse(source)
5
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: tagsnip
3
+ Version: 1.0.0
4
+ Summary: Python package for extracting tagged code snippets from local files or remote sources.
5
+ Author: Rostislav Brož
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/brozrost/tagsnip
8
+ Project-URL: Repository, https://github.com/brozrost/tagsnip
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.md
12
+ Requires-Dist: requests
13
+ Dynamic: license-file
14
+
15
+ `tagsnip` is a Python CLI tool for extracting tagged code snippets from local files or remote URLs, such as GitHub raw files. It serves as the backend for the tagsnip LaTeX package and enables dynamic inclusion of code snippets directly in LaTeX documents.
16
+
17
+ <div align="center">
18
+ <a href="https://github.com/brozrost/tagsnip/actions">
19
+ <img src="https://github.com/brozrost/tagsnip/actions/workflows/python-package.yml/badge.svg">
20
+ </a>
21
+ <a href="https://github.com/brozrost/tagsnip/graphs/contributors">
22
+ <img src="https://img.shields.io/github/contributors/brozrost/tagsnip">
23
+ </a>
24
+ <a href="https://github.com/brozrost/tagsnip/issues">
25
+ <img src="https://img.shields.io/github/issues/brozrost/tagsnip">
26
+ </a>
27
+ <a href="https://github.com/brozrost/tagsnip/pulls">
28
+ <img src="https://img.shields.io/github/issues-pr/brozrost/tagsnip">
29
+ </a>
30
+ </div>
31
+
32
+ ## Installation
33
+
34
+ Install `tagsnip` from PyPI:
35
+
36
+ ```sh
37
+ pip install tagsnip
38
+ ```
39
+
40
+ After installation, the command `tagsnip` should be available in your shell:
41
+
42
+ ```sh
43
+ tagsnip --help
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```sh
49
+ tagsnip -s <source> -t <tag> [-o <output>]
50
+ ```
51
+
52
+ ```sh
53
+ tagsnip -c <output>
54
+ ```
55
+
56
+ ### Command-line options
57
+ ```text
58
+ -s, --source Local file path or remote URL.
59
+ -t, --tag Snippet tag to extract.
60
+ -o, --output Optional output file path.
61
+ -c, --cleanup Removes temporary files at specified path.
62
+ ```
63
+
64
+ `--source` and `--tag` are mandatory arguments. If `--output` is provided, `tagsnip` writes the extracted snippet to the selected file. Otherwise, the snippet is written to `stdout`.
65
+
66
+ When `--output` is used, `tagsnip` also writes a metadata file next to the output file. For example, if the output file is `snippet.tmp`, then the metadata file is `snippet.tmp.meta`. Both files can be removed using the `--cleanup` option.
67
+
68
+ The metadata file contains the original start and end line numbers of the extracted snippet. This is used by the LaTeX package to preserve source line numbering.
69
+
70
+ ### Supported sources
71
+
72
+ - Local text files
73
+ - Remote text files available over HTTP or HTTPS
74
+ - GitHub raw file URLs
75
+
76
+ ### Tag format
77
+
78
+ `tagsnip` extracts code between two matching markers:
79
+
80
+ ```text
81
+ tagsnip-start <tag>
82
+ ...
83
+ tagsnip-end <tag>
84
+ ```
85
+
86
+ Tags are case-sensitive. Markers must appear on separate lines. The tag must follow the marker name after one space.
87
+
88
+ For example:
89
+
90
+ ```python
91
+ # tagsnip-start demo
92
+ print("This line will be extracted.")
93
+ # tagsnip-end demo
94
+ ```
95
+
96
+ The marker lines themselves are not included in the extracted snippet.
97
+
98
+ ### Error handling
99
+
100
+ `tagsnip` reports errors when:
101
+
102
+ - The tag is not found
103
+ - Start/end markers are mismatched
104
+ - Multiple start markers exist
105
+ - The source cannot be fetched
106
+
107
+ ## Quick example
108
+
109
+ ### Example source file
110
+
111
+ ```python
112
+ # tagsnip-start demo
113
+ def main():
114
+ x = 1
115
+ y = 2
116
+ print(x + y)
117
+
118
+ return 0
119
+ # tagsnip-end demo
120
+
121
+ if __name__ == "__main__":
122
+ import sys
123
+ sys.exit(main())
124
+ ```
125
+
126
+ ### CLI usage
127
+
128
+ Write the snippet to `stdout`:
129
+
130
+ ```bash
131
+ tagsnip -s example.py -t demo
132
+ ```
133
+
134
+ Write the snippet to a file:
135
+ ```bash
136
+ tagsnip -s example.py -t demo -o out/out.txt
137
+ ```
138
+
139
+ Get snippet from URL:
140
+ ```bash
141
+ tagsnip -s https://raw.githubusercontent.com/brozrost/tagsnip/main/docs/example.py -t demo
142
+ ```
143
+
144
+ Remove temporary files:
145
+ ```bash
146
+ tagsnip -c out/out.txt
147
+ ```
148
+
149
+ ### Python usage
150
+
151
+ ```python
152
+ from tagsnip import extractor
153
+
154
+ snippet = extractor.extract_from_file("example.py", "demo")
155
+ ```
156
+
157
+ ## Compatibility note
158
+
159
+ The Python package and the LaTeX package should be kept in sync. Backwards compatibility is not guaranteed.
@@ -0,0 +1,19 @@
1
+ LICENSE.md
2
+ README-pypi.md
3
+ README.md
4
+ pyproject.toml
5
+ setup.cfg
6
+ tagsnip/__init__.py
7
+ tagsnip/__main__.py
8
+ tagsnip/cli.py
9
+ tagsnip/extractor.py
10
+ tagsnip/fetcher.py
11
+ tagsnip/validate.py
12
+ tagsnip.egg-info/PKG-INFO
13
+ tagsnip.egg-info/SOURCES.txt
14
+ tagsnip.egg-info/dependency_links.txt
15
+ tagsnip.egg-info/entry_points.txt
16
+ tagsnip.egg-info/requires.txt
17
+ tagsnip.egg-info/top_level.txt
18
+ tests/test_extractor.py
19
+ tests/test_fetcher.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tagsnip = tagsnip.cli:main
@@ -0,0 +1 @@
1
+ requests
@@ -0,0 +1 @@
1
+ tagsnip
@@ -0,0 +1,91 @@
1
+ from tagsnip import extractor
2
+
3
+ PATH = "./docs/example.py"
4
+
5
+ TEXT = """
6
+ # tagsnip-start demo
7
+ def main():
8
+ x = 1
9
+ y = 2
10
+ print(x + y)
11
+
12
+ return 0
13
+ # tagsnip-end demo
14
+
15
+ if __name__ == "__main__":
16
+ import sys
17
+ sys.exit(main())"""
18
+
19
+ OUT = """def main():
20
+ x = 1
21
+ y = 2
22
+ print(x + y)
23
+
24
+ return 0"""
25
+
26
+ def test_extract_tagged_block():
27
+ snippet, first_line_num, last_line_num = extractor.extract_tagged_block(TEXT, "demo")
28
+ assert snippet == OUT
29
+
30
+ def test_extract_from_file():
31
+ snippet, first_line_num, last_line_num = extractor.extract_from_file(PATH, "tag1")
32
+ assert snippet == OUT
33
+
34
+ def test_missing_start_tag():
35
+ text = """
36
+ hello
37
+ # tagsnip-end 1
38
+ """
39
+
40
+ try:
41
+ extractor.extract_tagged_block(text, "1")
42
+ assert False, "Expected TagsnipError"
43
+ except extractor.TagsnipError as err:
44
+ assert str(err) == "Start tag not found for '1'"
45
+
46
+
47
+ def test_missing_end_tag():
48
+ text = """
49
+ # tagsnip-start 1
50
+ hello
51
+ """
52
+
53
+ try:
54
+ extractor.extract_tagged_block(text, "1")
55
+ assert False, "Expected TagsnipError"
56
+ except extractor.TagsnipError as err:
57
+ assert str(err) == "End tag not found for '1'"
58
+
59
+
60
+ def test_multiple_start_tags():
61
+ text = """
62
+ # tagsnip-start 1
63
+ hello
64
+ # tagsnip-start 1
65
+ world
66
+ # tagsnip-end 1
67
+ """
68
+
69
+ try:
70
+ extractor.extract_tagged_block(text, "1")
71
+ assert False, "Expected TagsnipError"
72
+ except extractor.TagsnipError as err:
73
+ assert str(err) == "Multiple start tags found for '1'"
74
+
75
+ def test_missing_file():
76
+ try:
77
+ extractor.extract_from_file("./does_not_exist.py", "1")
78
+ assert False, "Expected TagsnipError"
79
+ except extractor.TagsnipError:
80
+ pass
81
+
82
+ def main():
83
+ test_extract_tagged_block()
84
+ test_extract_from_file()
85
+ test_missing_start_tag()
86
+ test_missing_end_tag()
87
+ test_multiple_start_tags()
88
+ test_missing_file()
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,4 @@
1
+ from tagsnip import fetcher
2
+
3
+ URL = "https://raw.githubusercontent.com/brozrost/tagsnip/refs/heads/main/docs/example.py"
4
+