apar-parser 1.0.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.
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: apar-parser
3
+ Version: 1.0.0
4
+ Summary: Parse IBM APAR information from web pages
5
+ Author: apar-parser contributors
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: apar,ibm,mainframe,parser
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.8
19
+ Requires-Dist: beautifulsoup4>=4.12.0
20
+ Requires-Dist: requests>=2.31.0
21
+ Provides-Extra: gui
22
+ Requires-Dist: tk; extra == 'gui'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # APAR Parser
26
+
27
+ Parse IBM APAR (Authorized Program Analysis Report) information from IBM support pages.
28
+
29
+ ## Installation
30
+
31
+ ### Using uvx (Recommended)
32
+
33
+ **Note:** If you don't have uvx installed, see [Installing uv/uvx](#installing-uvuvx) below.
34
+
35
+ ```bash
36
+ # Run directly
37
+ uvx apar-parser -i apar_list.txt -o output_dir
38
+
39
+ # Or run from local directory
40
+ uvx --from . apar-parser -i apar_list.txt -o output_dir
41
+ ```
42
+
43
+ ### Using pip
44
+
45
+ ```bash
46
+ # Install from PyPI
47
+ pip install apar-parser
48
+
49
+ # Or install from local directory
50
+ pip install .
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Process APAR list from file
56
+ ```bash
57
+ apar-parser -i apar_list.txt -o output_dir
58
+ ```
59
+
60
+ ### Process single APAR
61
+ ```bash
62
+ apar-parser -a OA41368 -o output_dir
63
+ ```
64
+
65
+ ### Output formats
66
+
67
+ **Text format (default):**
68
+ ```bash
69
+ apar-parser -i apar_list.txt -o output_dir
70
+ ```
71
+
72
+ **JSON format (structured data):**
73
+ ```bash
74
+ apar-parser -i apar_list.txt -o output_dir -f json
75
+ ```
76
+
77
+ ### GUI mode (Windows only, requires tkinter)
78
+ ```bash
79
+ apar-parser --gui
80
+ ```
81
+
82
+ ## Input File Format
83
+
84
+ Plain text file with one APAR number per line:
85
+ ```
86
+ OA41368
87
+ OA36415
88
+ OA12345
89
+ ```
90
+
91
+ ## Output
92
+
93
+ ### Text format
94
+ Creates text files in the output directory:
95
+ - `{APAR}.txt` - Successfully parsed APAR
96
+ - `{APAR}.notfound.txt` - APAR not found
97
+ - `{APAR}.logon.txt` - Requires IBM ID login
98
+
99
+ ### JSON format
100
+ Creates JSON files with structured data:
101
+ ```json
102
+ {
103
+ "apar_number": "OA41368",
104
+ "title": "ABEND0C4-11 IN EDGFLTS...",
105
+ "modified_date": "21 May 2013",
106
+ "apar_information": {
107
+ "apar_number": "OA41368",
108
+ "reported_component_name": "DFSMSRMM",
109
+ ...
110
+ },
111
+ "problem_summary": "...",
112
+ "problem_conclusion": "..."
113
+ }
114
+ ```
115
+
116
+ ## Options
117
+
118
+ - `-i, --input`: Input file with APAR numbers (one per line)
119
+ - `-o, --output`: Output directory (required unless using --gui)
120
+ - `-a, --apar`: Single APAR number to process
121
+ - `-f, --format`: Output format: `txt` or `json` (default: txt)
122
+ - `--gui`: Use GUI for file selection (Windows only, requires tkinter)
123
+
124
+ ---
125
+
126
+ ## Installing uv/uvx
127
+
128
+ **macOS/Linux:**
129
+ ```bash
130
+ curl -LsSf https://astral.sh/uv/install.sh | sh
131
+ ```
132
+
133
+ **Windows:**
134
+ ```powershell
135
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
136
+ ```
137
+
138
+ Or with pip:
139
+ ```bash
140
+ pip install uv
141
+ ```
142
+
143
+ ## Disclaimer
144
+
145
+ This is an unofficial tool and is not affiliated with or endorsed by IBM. This tool parses publicly available APAR information from IBM support pages for convenience purposes only. Users are responsible for complying with IBM's terms of service and acceptable use policies when using this tool.
146
+
147
+ ## Acknowledgments
148
+
149
+ Special thanks to *Tom Tsai*, the original author of this function.
150
+
151
+
@@ -0,0 +1,6 @@
1
+ apar_parser.py,sha256=XaIEZpcXD-1KpR-XCAocW1RUixX-hjDdNVOL-ww04v8,12454
2
+ apar_parser-1.0.0.dist-info/METADATA,sha256=YOu0ngJsOxKVSAGgBjHk5UL4VAY7k3J7_KuMWi24WH4,3443
3
+ apar_parser-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
4
+ apar_parser-1.0.0.dist-info/entry_points.txt,sha256=gAVFoKgY1v8RrNUpa3efMV3JGtd7Zl7r7DOGUY5w23M,49
5
+ apar_parser-1.0.0.dist-info/licenses/LICENSE,sha256=GVROAyQKp97P9TBQiIMH6jFyz3GKPUgk1H-wa5PFjoc,1081
6
+ apar_parser-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ apar-parser = apar_parser:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 apar-parser contributors
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.
apar_parser.py ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env python3
2
+ """APAR Parser - Extract IBM APAR information from web pages"""
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import requests
10
+ from bs4 import BeautifulSoup
11
+
12
+
13
+ class APARParser:
14
+ BASE_URL = "https://www.ibm.com/support/pages/apar/"
15
+
16
+ INFO_TYPES = {
17
+ 'type1': ['APAR Information'],
18
+ 'type2': ['Error description', 'Local fix', 'Problem summary',
19
+ 'Problem conclusion', 'Temporary fix', 'Comments', 'Modules/Macros'],
20
+ 'type3': ['Fix information', 'Applicable component levels'],
21
+ 'type4': ['APAR status']
22
+ }
23
+
24
+ def __init__(self, output_dir: Path, output_format: str = 'txt'):
25
+ self.output_dir = output_dir
26
+ self.output_format = output_format
27
+ self.output_dir.mkdir(parents=True, exist_ok=True)
28
+
29
+ def fetch_apar(self, apar_number: str) -> Optional[str]:
30
+ """Fetch APAR page content"""
31
+ try:
32
+ response = requests.get(f"{self.BASE_URL}{apar_number}", timeout=30)
33
+ response.raise_for_status()
34
+ return response.text.replace('<p/>', '')
35
+ except requests.RequestException as e:
36
+ print(f"Error fetching {apar_number}: {e}")
37
+ return None
38
+
39
+ def parse_apar(self, apar_number: str, html: str) -> tuple[str, str | dict]:
40
+ """Parse APAR HTML and return status and content"""
41
+ soup = BeautifulSoup(html, 'html.parser')
42
+
43
+ if not soup.title:
44
+ return 'logon', html
45
+
46
+ if apar_number not in soup.title.text:
47
+ return 'notfound', html
48
+
49
+ if self.output_format == 'json':
50
+ content = self._extract_json(soup, apar_number)
51
+ else:
52
+ content = self._extract_content(soup, apar_number)
53
+
54
+ return 'success', content
55
+
56
+ def _extract_content(self, soup: BeautifulSoup, apar_number: str) -> str:
57
+ """Extract all APAR information"""
58
+ lines = []
59
+
60
+ # Title - split APAR number and title
61
+ title_text = soup.title.text
62
+ lines.append(title_text[:7]) # APAR number
63
+ if len(title_text) > 9:
64
+ lines.append(title_text[9:]) # Title after "OA41368: "
65
+
66
+ # Modified date
67
+ doc_info = soup.find(id='ibm-document-information')
68
+ if doc_info:
69
+ date_p = doc_info.find('p')
70
+ if date_p and len(date_p.contents) > 4:
71
+ # Keep original formatting, remove first newline char
72
+ date_text = date_p.contents[4][1:]
73
+ lines.append(f"Document Modified Date: {date_text}")
74
+
75
+ # Process sections
76
+ for h2 in soup.find_all("h2"):
77
+ section_name = h2.text
78
+
79
+ if section_name in self.INFO_TYPES['type1']:
80
+ lines.extend(self._parse_type1(h2))
81
+ elif section_name in self.INFO_TYPES['type2']:
82
+ lines.extend(self._parse_type2(h2, section_name))
83
+ elif section_name in self.INFO_TYPES['type3']:
84
+ lines.extend(self._parse_type3(h2, section_name))
85
+ elif section_name in self.INFO_TYPES['type4']:
86
+ lines.extend(self._parse_type4(h2, section_name))
87
+
88
+ return '\n'.join(lines)
89
+
90
+ def _extract_json(self, soup: BeautifulSoup, apar_number: str) -> dict:
91
+ """Extract APAR information as JSON"""
92
+ data = {'apar_number': apar_number}
93
+
94
+ # Title
95
+ title_text = soup.title.text
96
+ data['title'] = title_text[9:] if len(title_text) > 9 else ''
97
+
98
+ # Modified date
99
+ doc_info = soup.find(id='ibm-document-information')
100
+ if doc_info:
101
+ date_p = doc_info.find('p')
102
+ if date_p and len(date_p.contents) > 4:
103
+ data['modified_date'] = date_p.contents[4].strip()
104
+
105
+ # Process sections
106
+ for h2 in soup.find_all("h2"):
107
+ section_name = h2.text
108
+ section_key = section_name.lower().replace(' ', '_')
109
+
110
+ if section_name in self.INFO_TYPES['type1']:
111
+ data[section_key] = self._parse_type1_json(h2)
112
+ elif section_name in self.INFO_TYPES['type2']:
113
+ data[section_key] = self._parse_type2_json(h2)
114
+ elif section_name in self.INFO_TYPES['type3']:
115
+ data[section_key] = self._parse_type3_json(h2)
116
+ elif section_name in self.INFO_TYPES['type4']:
117
+ data[section_key] = self._parse_type4_json(h2)
118
+
119
+ return data
120
+
121
+ def _parse_type1_json(self, h2) -> dict:
122
+ """Parse APAR Information as JSON"""
123
+ info = {}
124
+ div = h2.find_next_sibling('div')
125
+ if not div:
126
+ return info
127
+
128
+ ul = div.find('ul')
129
+ if ul:
130
+ for li in ul.find_all('li'):
131
+ h3 = li.find('h3')
132
+ p = li.find('p')
133
+ if h3:
134
+ key = h3.text.lower().replace(' ', '_')
135
+ info[key] = p.text if p else ''
136
+
137
+ ul = div.find_next_sibling('ul')
138
+ if ul:
139
+ routes = {'from': [], 'to': []}
140
+ for li in ul.find_all('li'):
141
+ h3 = li.find('h3')
142
+ p = li.find('p')
143
+ if h3 and 'FROM' in h3.text:
144
+ routes['from'] = p.text.strip().split() if p else []
145
+ elif h3 and 'TO' in h3.text:
146
+ routes['to'] = p.text.strip().split() if p else []
147
+ info['sysroute'] = routes
148
+
149
+ return info
150
+
151
+ def _parse_type2_json(self, h2) -> str:
152
+ """Parse pre-formatted sections as JSON"""
153
+ ul = h2.find_next_sibling('ul')
154
+ if ul:
155
+ li = ul.find('li')
156
+ if li:
157
+ pre = li.find('pre')
158
+ if pre:
159
+ return pre.text
160
+ return ''
161
+
162
+ def _parse_type3_json(self, h2) -> list[dict]:
163
+ """Parse tabular sections as JSON"""
164
+ items = []
165
+ ul = h2.find_next_sibling('ul')
166
+ if ul:
167
+ for li in ul.find_all('li'):
168
+ parts = [child.text for child in li.children if hasattr(child, 'text')]
169
+ if parts:
170
+ items.append({'fields': parts})
171
+ return items
172
+
173
+ def _parse_type4_json(self, h2) -> str:
174
+ """Parse APAR status as JSON"""
175
+ ul = h2.find_next_sibling('ul')
176
+ if ul:
177
+ h3 = ul.find('h3')
178
+ if h3:
179
+ return h3.text
180
+ return ''
181
+
182
+ def _parse_type1(self, h2) -> list[str]:
183
+ """Parse APAR Information section"""
184
+ lines = [h2.text]
185
+ div = h2.find_next_sibling('div')
186
+ if not div:
187
+ return lines
188
+
189
+ # First ul - basic info
190
+ ul = div.find('ul')
191
+ if ul:
192
+ for li in ul.find_all('li'):
193
+ h3 = li.find('h3')
194
+ p = li.find('p')
195
+ if h3:
196
+ lines.append(f"{h3.text}\t{p.text}" if p else h3.text)
197
+
198
+ lines.append('')
199
+
200
+ # Second ul - relationships
201
+ ul = div.find_next_sibling('ul')
202
+ if ul:
203
+ for li in ul.find_all('li'):
204
+ h3 = li.find('h3')
205
+ p = li.find('p')
206
+ if h3:
207
+ lines.append(f"{h3.text}\t{p.text[1:]}" if p else h3.text)
208
+
209
+ lines.extend(['', '--', ''])
210
+ return lines
211
+
212
+ def _parse_type2(self, h2, section_name: str) -> list[str]:
213
+ """Parse sections with pre-formatted text"""
214
+ lines = [section_name]
215
+ ul = h2.find_next_sibling('ul')
216
+ if ul:
217
+ li = ul.find('li')
218
+ if li:
219
+ pre = li.find('pre')
220
+ if pre and pre.text.strip():
221
+ lines.append(pre.text.rstrip('\n'))
222
+ lines.extend(['', '--', ''])
223
+ return lines
224
+
225
+ def _parse_type3(self, h2, section_name: str) -> list[str]:
226
+ """Parse sections with tabular data"""
227
+ lines = [section_name]
228
+ ul = h2.find_next_sibling('ul')
229
+ if ul:
230
+ for li in ul.find_all('li'):
231
+ parts = [child.text for child in li.children if hasattr(child, 'text')]
232
+ lines.append('\t'.join(parts))
233
+ lines.extend(['', ''])
234
+ return lines
235
+
236
+ def _parse_type4(self, h2, section_name: str) -> list[str]:
237
+ """Parse APAR status section"""
238
+ lines = [section_name]
239
+ ul = h2.find_next_sibling('ul')
240
+ if ul:
241
+ h3 = ul.find('h3')
242
+ if h3:
243
+ lines.append(h3.text)
244
+ lines.extend(['', ''])
245
+ return lines
246
+
247
+ def save_apar(self, apar_number: str, content: str | dict, status: str = 'success'):
248
+ """Save APAR content to file"""
249
+ suffix = '' if status == 'success' else f'.{status}'
250
+
251
+ if self.output_format == 'json':
252
+ output_file = self.output_dir / f"{apar_number}{suffix}.json"
253
+ try:
254
+ if isinstance(content, dict):
255
+ output_file.write_text(json.dumps(content, indent=2, ensure_ascii=False))
256
+ else:
257
+ output_file.write_text(content, encoding='utf-8', errors='ignore')
258
+ print(f"{apar_number} {'processing..' if status == 'success' else status}")
259
+ except IOError as e:
260
+ print(f"Error saving {apar_number}: {e}")
261
+ else:
262
+ output_file = self.output_dir / f"{apar_number}{suffix}.txt"
263
+ try:
264
+ if isinstance(content, str):
265
+ content = content.rstrip('\n') + '\n\n\n'
266
+ output_file.write_text(content, encoding='utf-8', errors='ignore')
267
+ print(f"{apar_number} {'processing..' if status == 'success' else status}")
268
+ except IOError as e:
269
+ print(f"Error saving {apar_number}: {e}")
270
+
271
+ def process_apar(self, apar_number: str):
272
+ """Process single APAR"""
273
+ apar_number = apar_number.strip()[:7].upper()
274
+
275
+ html = self.fetch_apar(apar_number)
276
+ if not html:
277
+ return
278
+
279
+ status, content = self.parse_apar(apar_number, html)
280
+ self.save_apar(apar_number, content, status)
281
+
282
+ def process_file(self, input_file: Path):
283
+ """Process APAR list from file"""
284
+ try:
285
+ with input_file.open('r') as f:
286
+ for line in f:
287
+ if line.strip():
288
+ self.process_apar(line)
289
+ except IOError as e:
290
+ print(f"Error reading input file: {e}")
291
+ sys.exit(1)
292
+
293
+
294
+ def main():
295
+ parser = argparse.ArgumentParser(description='Parse IBM APAR information')
296
+ parser.add_argument('-i', '--input', type=Path, help='Input file with APAR numbers')
297
+ parser.add_argument('-o', '--output', type=Path, help='Output directory')
298
+ parser.add_argument('-a', '--apar', help='Single APAR number to process')
299
+ parser.add_argument('-f', '--format', choices=['txt', 'json'], default='txt',
300
+ help='Output format (default: txt)')
301
+ parser.add_argument('--gui', action='store_true', help='Use GUI for file selection')
302
+
303
+ args = parser.parse_args()
304
+
305
+ # GUI mode
306
+ if args.gui:
307
+ try:
308
+ import tkinter as tk
309
+ from tkinter import filedialog
310
+ root = tk.Tk()
311
+ root.withdraw()
312
+
313
+ output_dir = filedialog.askdirectory(title='Select output directory')
314
+ if not output_dir:
315
+ print('No output directory selected')
316
+ sys.exit(1)
317
+
318
+ input_file = filedialog.askopenfilename(title='Select APAR list file')
319
+ if not input_file:
320
+ print('No input file selected')
321
+ sys.exit(1)
322
+
323
+ args.output = Path(output_dir)
324
+ args.input = Path(input_file)
325
+ except ImportError:
326
+ print('tkinter not available')
327
+ sys.exit(1)
328
+
329
+ # Validate arguments
330
+ if not args.output:
331
+ parser.error('--output is required (or use --gui)')
332
+ if not args.apar and not args.input:
333
+ parser.error('Either --input or --apar must be specified')
334
+
335
+ # Process
336
+ apar_parser = APARParser(args.output, args.format)
337
+
338
+ if args.apar:
339
+ apar_parser.process_apar(args.apar)
340
+ else:
341
+ apar_parser.process_file(args.input)
342
+
343
+
344
+ if __name__ == '__main__':
345
+ main()