flamegraph-textual 0.1.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.
Files changed (24) hide show
  1. flamegraph_textual-0.1.0/PKG-INFO +170 -0
  2. flamegraph_textual-0.1.0/README.md +148 -0
  3. flamegraph_textual-0.1.0/flamegraph_textual/__init__.py +25 -0
  4. flamegraph_textual-0.1.0/flamegraph_textual/colors.py +67 -0
  5. flamegraph_textual-0.1.0/flamegraph_textual/const.py +4 -0
  6. flamegraph_textual-0.1.0/flamegraph_textual/exceptions.py +14 -0
  7. flamegraph_textual-0.1.0/flamegraph_textual/models.py +204 -0
  8. flamegraph_textual-0.1.0/flamegraph_textual/parsers/__init__.py +25 -0
  9. flamegraph_textual-0.1.0/flamegraph_textual/parsers/pprof_parser.py +352 -0
  10. flamegraph_textual-0.1.0/flamegraph_textual/parsers/profile_pb2.py +40 -0
  11. flamegraph_textual-0.1.0/flamegraph_textual/parsers/stackcollapse_parser.py +134 -0
  12. flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/__init__.py +4 -0
  13. flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/parser.py +1 -0
  14. flamegraph_textual-0.1.0/flamegraph_textual/pprof_parser/profile_pb2.py +1 -0
  15. flamegraph_textual-0.1.0/flamegraph_textual/render/__init__.py +3 -0
  16. flamegraph_textual-0.1.0/flamegraph_textual/render/app.py +257 -0
  17. flamegraph_textual-0.1.0/flamegraph_textual/render/flamegraph.py +381 -0
  18. flamegraph_textual-0.1.0/flamegraph_textual/render/framedetail.py +452 -0
  19. flamegraph_textual-0.1.0/flamegraph_textual/render/header.py +108 -0
  20. flamegraph_textual-0.1.0/flamegraph_textual/render/tabs.py +5 -0
  21. flamegraph_textual-0.1.0/flamegraph_textual/runtime.py +22 -0
  22. flamegraph_textual-0.1.0/flamegraph_textual/utils.py +12 -0
  23. flamegraph_textual-0.1.0/flamegraph_textual/view.py +164 -0
  24. flamegraph_textual-0.1.0/pyproject.toml +24 -0
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.3
2
+ Name: flamegraph-textual
3
+ Version: 0.1.0
4
+ Summary: Terminal flamegraph renderer built for Textual
5
+ License: GPLv3
6
+ Author: laixintao
7
+ Author-email: laixintaoo@gmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Dist: iteround (>=1.0.4,<2.0.0)
16
+ Requires-Dist: protobuf (>=4.25,<5.0)
17
+ Requires-Dist: rich (>=13.6.0,<14.0.0)
18
+ Requires-Dist: textual (>=0.37.1,<0.38.0)
19
+ Requires-Dist: typing-extensions (>=4.7.1,<5.0.0)
20
+ Description-Content-Type: text/markdown
21
+
22
+ # flamegraph-textual
23
+
24
+ `flamegraph-textual` is an interactive flamegraph component for
25
+ [Textual](https://github.com/Textualize/textual).
26
+
27
+ ![](./docs/flamegraph-textual.png)
28
+
29
+ It is the rendering library extracted from
30
+ [flameshow](https://github.com/laixintao/flameshow). Use it when you want to
31
+ embed a terminal flamegraph inside your own Textual app instead of launching a
32
+ standalone viewer.
33
+
34
+ ## Install
35
+
36
+ ```shell
37
+ pip install flamegraph-textual
38
+ ```
39
+
40
+ ## What It Does
41
+
42
+ - Renders flamegraphs as a Textual widget
43
+ - Parses profile input for you
44
+ - Supports keyboard and mouse navigation
45
+ - Supports multiple sample types when present in the profile
46
+ - Works with bundled demo data or your own files
47
+
48
+ ## Quick Start
49
+
50
+ `FlameGraphView` is the main entrypoint. Pass it raw profile data and a
51
+ filename. The library parses the content internally.
52
+
53
+ ```python
54
+ from pathlib import Path
55
+
56
+ from textual.app import App, ComposeResult
57
+
58
+ from flamegraph_textual import FlameGraphView
59
+
60
+
61
+ class Demo(App):
62
+ def compose(self) -> ComposeResult:
63
+ profile_bytes = Path("profile.out").read_bytes()
64
+ yield FlameGraphView(profile_bytes, filename="profile.out")
65
+
66
+
67
+ Demo().run()
68
+ ```
69
+
70
+ For stackcollapse text input, passing `str` also works:
71
+
72
+ ```python
73
+ from pathlib import Path
74
+
75
+ from flamegraph_textual import FlameGraphView
76
+
77
+ profile_text = Path("stacks.txt").read_text(encoding="utf-8")
78
+ widget = FlameGraphView(profile_text, filename="stacks.txt")
79
+ ```
80
+
81
+ ## Supported Input Formats
82
+
83
+ - pprof protobuf profiles
84
+ - stackcollapse text
85
+
86
+ The parser selection is automatic through:
87
+ [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
88
+
89
+ ## Try It Immediately
90
+
91
+ This repo includes sample profiles under:
92
+
93
+ - [tests/pprof_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/pprof_data)
94
+ - [tests/stackcollapse_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/stackcollapse_data)
95
+
96
+ Run the bundled examples with no setup:
97
+
98
+ ```shell
99
+ python examples/pprof_binary.py
100
+ python examples/pprof_binary.py --sample goroutine
101
+ python examples/pprof_binary.py --sample heap
102
+
103
+ python examples/stackcollapse_text.py
104
+ python examples/stackcollapse_text.py --sample simple
105
+ python examples/stackcollapse_text.py --sample perf
106
+ ```
107
+
108
+ You can still pass your own file path:
109
+
110
+ ```shell
111
+ python examples/pprof_binary.py /path/to/profile.out
112
+ python examples/stackcollapse_text.py /path/to/stacks.txt
113
+ ```
114
+
115
+ ## Main API
116
+
117
+ Most users only need:
118
+
119
+ - [FlameGraphView](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/view.py)
120
+ - [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
121
+
122
+ Other exports are available if you want lower-level control:
123
+
124
+ - `FlameGraph`
125
+ - `FlameGraphScroll`
126
+ - `Frame`
127
+ - `Profile`
128
+ - `SampleType`
129
+
130
+ See:
131
+ [__init__.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/__init__.py)
132
+
133
+ ## Controls
134
+
135
+ Inside the widget:
136
+
137
+ - `j` / `k` / `h` / `l` or arrow keys move selection
138
+ - `Enter` zooms in
139
+ - `Esc` zooms out
140
+ - `Tab` switches sample type
141
+ - `i` opens the detail screen when mounted inside a Textual app
142
+ - Mouse hover updates frame details
143
+ - Mouse click zooms into a frame
144
+
145
+ ## Regenerate Protobuf Bindings
146
+
147
+ The canonical pprof schema lives in:
148
+ [profile.proto](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/proto/profile.proto)
149
+
150
+ The generated Python module lives in:
151
+ [profile_pb2.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/profile_pb2.py)
152
+
153
+ Regenerate it with:
154
+
155
+ ```shell
156
+ poetry add --group dev grpcio-tools
157
+ poetry run python -m grpc_tools.protoc \
158
+ -I proto \
159
+ --python_out=flamegraph_textual/parsers \
160
+ proto/profile.proto
161
+ ```
162
+
163
+ ## Development
164
+
165
+ Run tests with:
166
+
167
+ ```shell
168
+ pytest -q
169
+ ```
170
+
@@ -0,0 +1,148 @@
1
+ # flamegraph-textual
2
+
3
+ `flamegraph-textual` is an interactive flamegraph component for
4
+ [Textual](https://github.com/Textualize/textual).
5
+
6
+ ![](./docs/flamegraph-textual.png)
7
+
8
+ It is the rendering library extracted from
9
+ [flameshow](https://github.com/laixintao/flameshow). Use it when you want to
10
+ embed a terminal flamegraph inside your own Textual app instead of launching a
11
+ standalone viewer.
12
+
13
+ ## Install
14
+
15
+ ```shell
16
+ pip install flamegraph-textual
17
+ ```
18
+
19
+ ## What It Does
20
+
21
+ - Renders flamegraphs as a Textual widget
22
+ - Parses profile input for you
23
+ - Supports keyboard and mouse navigation
24
+ - Supports multiple sample types when present in the profile
25
+ - Works with bundled demo data or your own files
26
+
27
+ ## Quick Start
28
+
29
+ `FlameGraphView` is the main entrypoint. Pass it raw profile data and a
30
+ filename. The library parses the content internally.
31
+
32
+ ```python
33
+ from pathlib import Path
34
+
35
+ from textual.app import App, ComposeResult
36
+
37
+ from flamegraph_textual import FlameGraphView
38
+
39
+
40
+ class Demo(App):
41
+ def compose(self) -> ComposeResult:
42
+ profile_bytes = Path("profile.out").read_bytes()
43
+ yield FlameGraphView(profile_bytes, filename="profile.out")
44
+
45
+
46
+ Demo().run()
47
+ ```
48
+
49
+ For stackcollapse text input, passing `str` also works:
50
+
51
+ ```python
52
+ from pathlib import Path
53
+
54
+ from flamegraph_textual import FlameGraphView
55
+
56
+ profile_text = Path("stacks.txt").read_text(encoding="utf-8")
57
+ widget = FlameGraphView(profile_text, filename="stacks.txt")
58
+ ```
59
+
60
+ ## Supported Input Formats
61
+
62
+ - pprof protobuf profiles
63
+ - stackcollapse text
64
+
65
+ The parser selection is automatic through:
66
+ [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
67
+
68
+ ## Try It Immediately
69
+
70
+ This repo includes sample profiles under:
71
+
72
+ - [tests/pprof_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/pprof_data)
73
+ - [tests/stackcollapse_data](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/tests/stackcollapse_data)
74
+
75
+ Run the bundled examples with no setup:
76
+
77
+ ```shell
78
+ python examples/pprof_binary.py
79
+ python examples/pprof_binary.py --sample goroutine
80
+ python examples/pprof_binary.py --sample heap
81
+
82
+ python examples/stackcollapse_text.py
83
+ python examples/stackcollapse_text.py --sample simple
84
+ python examples/stackcollapse_text.py --sample perf
85
+ ```
86
+
87
+ You can still pass your own file path:
88
+
89
+ ```shell
90
+ python examples/pprof_binary.py /path/to/profile.out
91
+ python examples/stackcollapse_text.py /path/to/stacks.txt
92
+ ```
93
+
94
+ ## Main API
95
+
96
+ Most users only need:
97
+
98
+ - [FlameGraphView](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/view.py)
99
+ - [parse](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/__init__.py)
100
+
101
+ Other exports are available if you want lower-level control:
102
+
103
+ - `FlameGraph`
104
+ - `FlameGraphScroll`
105
+ - `Frame`
106
+ - `Profile`
107
+ - `SampleType`
108
+
109
+ See:
110
+ [__init__.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/__init__.py)
111
+
112
+ ## Controls
113
+
114
+ Inside the widget:
115
+
116
+ - `j` / `k` / `h` / `l` or arrow keys move selection
117
+ - `Enter` zooms in
118
+ - `Esc` zooms out
119
+ - `Tab` switches sample type
120
+ - `i` opens the detail screen when mounted inside a Textual app
121
+ - Mouse hover updates frame details
122
+ - Mouse click zooms into a frame
123
+
124
+ ## Regenerate Protobuf Bindings
125
+
126
+ The canonical pprof schema lives in:
127
+ [profile.proto](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/proto/profile.proto)
128
+
129
+ The generated Python module lives in:
130
+ [profile_pb2.py](/Users/xintao.lai/Programs/flameshow-all/flamegraph-textual/flamegraph_textual/parsers/profile_pb2.py)
131
+
132
+ Regenerate it with:
133
+
134
+ ```shell
135
+ poetry add --group dev grpcio-tools
136
+ poetry run python -m grpc_tools.protoc \
137
+ -I proto \
138
+ --python_out=flamegraph_textual/parsers \
139
+ proto/profile.proto
140
+ ```
141
+
142
+ ## Development
143
+
144
+ Run tests with:
145
+
146
+ ```shell
147
+ pytest -q
148
+ ```
@@ -0,0 +1,25 @@
1
+ __version__ = "0.1.0"
2
+
3
+ from flamegraph_textual.models import Frame, Profile, SampleType
4
+ from flamegraph_textual.parsers import parse
5
+ from flamegraph_textual.render.app import FlameGraphScroll, FlameshowApp
6
+ from flamegraph_textual.render.flamegraph import FlameGraph, FrameMap, add_array
7
+ from flamegraph_textual.view import FlameGraphView
8
+
9
+
10
+ class FlameGraphApp(FlameshowApp):
11
+ """Convenience alias for embedding a full flamegraph app."""
12
+
13
+
14
+ __all__ = [
15
+ "FlameGraph",
16
+ "FlameGraphApp",
17
+ "FlameGraphScroll",
18
+ "FlameGraphView",
19
+ "Frame",
20
+ "FrameMap",
21
+ "Profile",
22
+ "SampleType",
23
+ "add_array",
24
+ "parse",
25
+ ]
@@ -0,0 +1,67 @@
1
+ import random
2
+ import logging
3
+
4
+ from textual.color import Color
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class ColorPlatteBase:
10
+ def __init__(self):
11
+ self.assigned_color = {}
12
+
13
+ def get_color(self, key):
14
+ if key not in self.assigned_color:
15
+ self.assigned_color[key] = self.assign_color(key)
16
+ return self.assigned_color[key]
17
+
18
+ def assign_color(self, key):
19
+ raise NotImplementedError
20
+
21
+
22
+ class LinaerColorPlatte(ColorPlatteBase):
23
+ def __init__(
24
+ self,
25
+ start_color=Color.parse("#CD0000"),
26
+ end_color=Color.parse("#FFE637"),
27
+ ) -> None:
28
+ super().__init__()
29
+ self.assigned_color = {}
30
+ self.start_color = start_color
31
+ self.end_color = end_color
32
+ self.index = 0
33
+ self.platte = self.generate_platte()
34
+
35
+ def assign_color(self, key):
36
+ color = self.platte[self.index]
37
+ self.index += 1
38
+ if self.index == len(self.platte):
39
+ self.index = 0
40
+
41
+ logger.debug("assign color=%s", color)
42
+ return color
43
+
44
+ def generate_platte(self):
45
+ color_platte = []
46
+ for factor in range(0, 100, 5):
47
+ color_platte.append(
48
+ self.start_color.blend(self.end_color, factor / 100)
49
+ )
50
+ return color_platte
51
+
52
+
53
+ class FlameGraphRandomColorPlatte(ColorPlatteBase):
54
+ def __init__(self) -> None:
55
+ super().__init__()
56
+ self.assigned_color = {}
57
+
58
+ def assign_color(self, *args):
59
+ return Color(
60
+ 205 + int(50 * random.random()),
61
+ 0 + int(230 * random.random()),
62
+ 0 + int(55 * random.random()),
63
+ )
64
+
65
+
66
+ flamegraph_random_color_platte = FlameGraphRandomColorPlatte()
67
+ linaer_color_platte = LinaerColorPlatte()
@@ -0,0 +1,4 @@
1
+ VIEW_INFO_COLOR = "#ffffff"
2
+ VIEW_INFO_OTHER_COLOR = "#8884FF"
3
+ SELECTED_PARENTS_BG_COLOR_BLEND_TO = "#8b0000"
4
+ SELECTED_PARENTS_BG_COLOR_BLEND_FACTOR = 0.5
@@ -0,0 +1,14 @@
1
+ class FlamegraphTextualException(Exception):
2
+ """Base exception for flamegraph_textual."""
3
+
4
+
5
+ class ProfileParseException(FlamegraphTextualException):
6
+ """Can not parse the profile."""
7
+
8
+
9
+ class UsageError(FlamegraphTextualException):
10
+ """Usage Error."""
11
+
12
+
13
+ class RenderException(FlamegraphTextualException):
14
+ """Error during flamegraph render."""
@@ -0,0 +1,204 @@
1
+ from dataclasses import dataclass, field
2
+ import datetime
3
+ import logging
4
+ import time
5
+ from typing import Dict, List, Set
6
+ from typing_extensions import Self
7
+
8
+ from rich.style import Style
9
+ from rich.text import Text
10
+
11
+ from flamegraph_textual.utils import sizeof
12
+
13
+ from flamegraph_textual.runtime import r
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class Frame:
20
+ def __init__(
21
+ self, name, _id, children=None, parent=None, values=None, root=None
22
+ ) -> None:
23
+ self.name = name
24
+ self._id = _id
25
+ if children:
26
+ self.children = children
27
+ else:
28
+ self.children = []
29
+ self.parent = parent
30
+ if not values:
31
+ self.values = []
32
+ else:
33
+ self.values = values
34
+
35
+ self.root = root
36
+
37
+ def pile_up(self, childstack: Self):
38
+ childstack.parent = self
39
+
40
+ for exist_child in self.children:
41
+ # added to exist, no need to create one
42
+ if exist_child.name == childstack.name:
43
+ # some cases, childstack.children total value not equal to
44
+ # childstack.values
45
+ # so, we need to add values of "parent" instead of add values
46
+ # by every child
47
+ exist_child.values = list(
48
+ map(sum, zip(exist_child.values, childstack.values))
49
+ )
50
+
51
+ for new_child in childstack.children:
52
+ exist_child.pile_up(new_child)
53
+ return
54
+
55
+ self.children.append(childstack)
56
+
57
+ def __eq__(self, other):
58
+ if isinstance(other, Frame):
59
+ return self._id == other._id
60
+ return False
61
+
62
+ @property
63
+ def display_color(self):
64
+ return r.get_color(self.color_key)
65
+
66
+ def humanize(self, sample_unit, value):
67
+ display_value = value
68
+ if sample_unit == "bytes":
69
+ display_value = sizeof(value)
70
+
71
+ return display_value
72
+
73
+ def __repr__(self) -> str:
74
+ return f"<Frame #{self._id} {self.name}>"
75
+
76
+ def render_detail(self, sample_index: int, sample_unit: str):
77
+ """
78
+ render stacked information
79
+ """
80
+ detail = []
81
+ frame = self
82
+ while frame:
83
+ lines = self.render_one_frame_detail(
84
+ frame, sample_index, sample_unit
85
+ )
86
+ for line in lines:
87
+ detail.append(
88
+ Text.assemble(
89
+ (" ", Style(bgcolor=frame.display_color.rich_color)),
90
+ " ",
91
+ line,
92
+ )
93
+ )
94
+ frame = frame.parent
95
+
96
+ return Text.assemble(*detail)
97
+
98
+ def render_one_frame_detail(
99
+ self, frame, sample_index: int, sample_unit: str
100
+ ):
101
+ raise NotImplementedError
102
+
103
+ @property
104
+ def title(self) -> Text:
105
+ """Full name which will be displayed in the frame detail panel"""
106
+ return Text(self.name)
107
+
108
+ @property
109
+ def color_key(self):
110
+ """Same key will get the same color"""
111
+ return self.name
112
+
113
+ @property
114
+ def display_name(self):
115
+ """The name display on the flamegraph"""
116
+ return self.name
117
+
118
+
119
+ @dataclass
120
+ class SampleType:
121
+ sample_type: str = ""
122
+ sample_unit: str = ""
123
+
124
+
125
+ @dataclass
126
+ class Profile:
127
+ # required
128
+ filename: str
129
+ root_stack: Frame
130
+ highest_lines: int
131
+ # total samples is one top most sample, it's a list that contains all
132
+ # its parents all the way up
133
+ total_sample: int
134
+ sample_types: List[SampleType]
135
+ # int id mapping to Frame
136
+ id_store: Dict[int, Frame]
137
+
138
+ # optional
139
+ default_sample_type_index: int = -1
140
+ period_type: SampleType | None = None
141
+ period: int = 0
142
+ created_at: datetime.datetime | None = None
143
+
144
+ # init by post_init
145
+ lines: List = field(init=False)
146
+
147
+ frameid_to_lineno: Dict[int, int] = field(init=False)
148
+
149
+ # Frame grouped by same name
150
+ name_aggr: Dict[str, List[Frame]] = field(init=False)
151
+
152
+ def __post_init__(self):
153
+ """
154
+ init_lines must be called before render
155
+ """
156
+ t1 = time.time()
157
+ logger.info("start to create lines...")
158
+
159
+ root = self.root_stack
160
+
161
+ lines = [
162
+ [root],
163
+ ]
164
+ frameid_to_lineno = {0: 0}
165
+ current = root.children
166
+ line_no = 1
167
+
168
+ while len(current) > 0:
169
+ line = []
170
+ next_line = []
171
+
172
+ for child in current:
173
+ line.append(child)
174
+ frameid_to_lineno[child._id] = line_no
175
+ next_line.extend(child.children)
176
+
177
+ lines.append(line)
178
+ line_no += 1
179
+ current = next_line
180
+
181
+ t2 = time.time()
182
+ logger.info("create lines done, took %.2f seconds", t2 - t1)
183
+ self.lines = lines
184
+ self.frameid_to_lineno = frameid_to_lineno
185
+
186
+ self.name_aggr = self.get_name_aggr(self.root_stack)
187
+
188
+ def get_name_aggr(
189
+ self, start_frame: Frame, names: Set[str] | None = None
190
+ ) -> Dict[str, List[Frame]]:
191
+ name = start_frame.name
192
+
193
+ result = {}
194
+ if names is None:
195
+ names = set()
196
+ if name not in names:
197
+ result[name] = [start_frame]
198
+
199
+ for child in start_frame.children:
200
+ name_aggr = self.get_name_aggr(child, names | set([name]))
201
+ for key, value in name_aggr.items():
202
+ result.setdefault(key, []).extend(value)
203
+
204
+ return result
@@ -0,0 +1,25 @@
1
+ import logging
2
+
3
+ from flamegraph_textual.exceptions import ProfileParseException
4
+ from flamegraph_textual.parsers.pprof_parser import ProfileParser as PprofParser
5
+ from flamegraph_textual.parsers.stackcollapse_parser import StackCollapseParser
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ ALL_PARSERS = [PprofParser, StackCollapseParser]
11
+
12
+
13
+ def choose_parser(content: bytes):
14
+ for p in ALL_PARSERS:
15
+ if p.validate(content):
16
+ return p
17
+ raise ProfileParseException("Can not match any parser")
18
+
19
+
20
+ def parse(filecontent: bytes, filename):
21
+ parser_cls = choose_parser(filecontent)
22
+ logger.info("Using %s...", parser_cls)
23
+ parser = parser_cls(filename)
24
+ profile = parser.parse(filecontent)
25
+ return profile