apar-parser 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.
- apar_parser-1.0.0/.gitignore +9 -0
- apar_parser-1.0.0/LICENSE +21 -0
- apar_parser-1.0.0/PKG-INFO +151 -0
- apar_parser-1.0.0/README.md +127 -0
- apar_parser-1.0.0/SKILL.md +101 -0
- apar_parser-1.0.0/apar_parser.py +345 -0
- apar_parser-1.0.0/pyproject.toml +36 -0
|
@@ -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.
|
|
@@ -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,127 @@
|
|
|
1
|
+
# APAR Parser
|
|
2
|
+
|
|
3
|
+
Parse IBM APAR (Authorized Program Analysis Report) information from IBM support pages.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Using uvx (Recommended)
|
|
8
|
+
|
|
9
|
+
**Note:** If you don't have uvx installed, see [Installing uv/uvx](#installing-uvuvx) below.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Run directly
|
|
13
|
+
uvx apar-parser -i apar_list.txt -o output_dir
|
|
14
|
+
|
|
15
|
+
# Or run from local directory
|
|
16
|
+
uvx --from . apar-parser -i apar_list.txt -o output_dir
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Using pip
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install from PyPI
|
|
23
|
+
pip install apar-parser
|
|
24
|
+
|
|
25
|
+
# Or install from local directory
|
|
26
|
+
pip install .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
### Process APAR list from file
|
|
32
|
+
```bash
|
|
33
|
+
apar-parser -i apar_list.txt -o output_dir
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Process single APAR
|
|
37
|
+
```bash
|
|
38
|
+
apar-parser -a OA41368 -o output_dir
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Output formats
|
|
42
|
+
|
|
43
|
+
**Text format (default):**
|
|
44
|
+
```bash
|
|
45
|
+
apar-parser -i apar_list.txt -o output_dir
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**JSON format (structured data):**
|
|
49
|
+
```bash
|
|
50
|
+
apar-parser -i apar_list.txt -o output_dir -f json
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### GUI mode (Windows only, requires tkinter)
|
|
54
|
+
```bash
|
|
55
|
+
apar-parser --gui
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Input File Format
|
|
59
|
+
|
|
60
|
+
Plain text file with one APAR number per line:
|
|
61
|
+
```
|
|
62
|
+
OA41368
|
|
63
|
+
OA36415
|
|
64
|
+
OA12345
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Output
|
|
68
|
+
|
|
69
|
+
### Text format
|
|
70
|
+
Creates text files in the output directory:
|
|
71
|
+
- `{APAR}.txt` - Successfully parsed APAR
|
|
72
|
+
- `{APAR}.notfound.txt` - APAR not found
|
|
73
|
+
- `{APAR}.logon.txt` - Requires IBM ID login
|
|
74
|
+
|
|
75
|
+
### JSON format
|
|
76
|
+
Creates JSON files with structured data:
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"apar_number": "OA41368",
|
|
80
|
+
"title": "ABEND0C4-11 IN EDGFLTS...",
|
|
81
|
+
"modified_date": "21 May 2013",
|
|
82
|
+
"apar_information": {
|
|
83
|
+
"apar_number": "OA41368",
|
|
84
|
+
"reported_component_name": "DFSMSRMM",
|
|
85
|
+
...
|
|
86
|
+
},
|
|
87
|
+
"problem_summary": "...",
|
|
88
|
+
"problem_conclusion": "..."
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Options
|
|
93
|
+
|
|
94
|
+
- `-i, --input`: Input file with APAR numbers (one per line)
|
|
95
|
+
- `-o, --output`: Output directory (required unless using --gui)
|
|
96
|
+
- `-a, --apar`: Single APAR number to process
|
|
97
|
+
- `-f, --format`: Output format: `txt` or `json` (default: txt)
|
|
98
|
+
- `--gui`: Use GUI for file selection (Windows only, requires tkinter)
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Installing uv/uvx
|
|
103
|
+
|
|
104
|
+
**macOS/Linux:**
|
|
105
|
+
```bash
|
|
106
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Windows:**
|
|
110
|
+
```powershell
|
|
111
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Or with pip:
|
|
115
|
+
```bash
|
|
116
|
+
pip install uv
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Disclaimer
|
|
120
|
+
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
## Acknowledgments
|
|
124
|
+
|
|
125
|
+
Special thanks to *Tom Tsai*, the original author of this function.
|
|
126
|
+
|
|
127
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# APAR Parser Skill
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Parse IBM APAR (Authorized Program Analysis Report) information from IBM support pages and save to text or JSON format.
|
|
6
|
+
|
|
7
|
+
## Capabilities
|
|
8
|
+
|
|
9
|
+
- Fetch APAR information from IBM support website
|
|
10
|
+
- Parse APAR details including status, problem summary, fix information, etc.
|
|
11
|
+
- Support batch processing from input file
|
|
12
|
+
- Output in text format (with section separators) or structured JSON
|
|
13
|
+
- Handle special cases (login required, not found)
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Parse single APAR
|
|
18
|
+
```bash
|
|
19
|
+
apar-parser -a <APAR_NUMBER> -o <OUTPUT_DIR>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Parse multiple APARs from file
|
|
23
|
+
```bash
|
|
24
|
+
apar-parser -i <INPUT_FILE> -o <OUTPUT_DIR>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Output as JSON
|
|
28
|
+
```bash
|
|
29
|
+
apar-parser -a <APAR_NUMBER> -o <OUTPUT_DIR> -f json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Input Format
|
|
33
|
+
|
|
34
|
+
For batch processing, create a text file with one APAR number per line:
|
|
35
|
+
```
|
|
36
|
+
OA41368
|
|
37
|
+
OA36415
|
|
38
|
+
OA12345
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Output
|
|
42
|
+
|
|
43
|
+
### Text Format
|
|
44
|
+
Creates `{APAR}.txt` files with sections separated by `--`:
|
|
45
|
+
- APAR status
|
|
46
|
+
- Error description
|
|
47
|
+
- Problem summary
|
|
48
|
+
- Problem conclusion
|
|
49
|
+
- APAR Information (metadata)
|
|
50
|
+
- Fix information
|
|
51
|
+
- Applicable component levels
|
|
52
|
+
|
|
53
|
+
### JSON Format
|
|
54
|
+
Creates `{APAR}.json` files with structured data containing all fields as key-value pairs.
|
|
55
|
+
|
|
56
|
+
### Special Cases
|
|
57
|
+
- `{APAR}.notfound.txt` - APAR does not exist
|
|
58
|
+
- `{APAR}.logon.txt` - Requires IBM ID authentication
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Using uvx (recommended)
|
|
64
|
+
uvx apar-parser -a OA41368 -o output
|
|
65
|
+
|
|
66
|
+
# Using pip
|
|
67
|
+
pip install apar-parser
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## When to Use This Skill
|
|
71
|
+
|
|
72
|
+
- User needs to fetch IBM APAR information
|
|
73
|
+
- User wants to analyze multiple APARs
|
|
74
|
+
- User needs structured APAR data for further processing
|
|
75
|
+
- User is researching IBM system issues or fixes
|
|
76
|
+
|
|
77
|
+
## Example Scenarios
|
|
78
|
+
|
|
79
|
+
**Scenario 1: Single APAR lookup**
|
|
80
|
+
```
|
|
81
|
+
User: "Get information for APAR OA41368"
|
|
82
|
+
Agent: apar-parser -a OA41368 -o ./apar_data
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Scenario 2: Batch processing**
|
|
86
|
+
```
|
|
87
|
+
User: "Parse all APARs in this list"
|
|
88
|
+
Agent: apar-parser -i apar_list.txt -o ./apar_results
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Scenario 3: JSON output for analysis**
|
|
92
|
+
```
|
|
93
|
+
User: "Get APAR data in JSON format for analysis"
|
|
94
|
+
Agent: apar-parser -a OA41368 -o ./data -f json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Notes
|
|
98
|
+
|
|
99
|
+
- Requires internet connection to fetch from IBM support website
|
|
100
|
+
- Some APARs may require IBM ID login (will be saved as .logon.txt)
|
|
101
|
+
- APAR numbers are typically 7 characters (e.g., OA41368)
|
|
@@ -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()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "apar-parser"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Parse IBM APAR information from web pages"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "apar-parser contributors"}
|
|
10
|
+
]
|
|
11
|
+
keywords = ["ibm", "apar", "parser", "mainframe"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.8",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"requests>=2.31.0",
|
|
25
|
+
"beautifulsoup4>=4.12.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
apar-parser = "apar_parser:main"
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
gui = ["tk"]
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|