py-json-analyzer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,196 @@
1
+ from collections import Counter
2
+
3
+
4
+ def analyze_json(data):
5
+ def analyze_node(node):
6
+ if isinstance(node, dict):
7
+ children = {}
8
+ for key, val in node.items():
9
+ children[key] = analyze_node(val)
10
+ return {"type": "object", "children": children}
11
+ elif isinstance(node, list):
12
+ # Skip empty or null-only lists
13
+ non_empty_items = [item for item in node if item not in (None, {}, [], "")]
14
+ if not non_empty_items:
15
+ return {"type": "list", "child_type": "unknown"}
16
+
17
+ sample = non_empty_items[:20]
18
+ element_summaries = [analyze_node(item) for item in sample]
19
+ types = {e["type"] for e in element_summaries}
20
+
21
+ # List of primitives
22
+ if len(types) == 1 and all(
23
+ e["type"] not in {"object", "list"} for e in element_summaries
24
+ ):
25
+ return {"type": "list", "child_type": types.pop()}
26
+
27
+ # List of objects
28
+ if all(e["type"] == "object" for e in element_summaries):
29
+ merged, conflicts = merge_object_summaries(element_summaries)
30
+ return {
31
+ "type": "list",
32
+ "child": {
33
+ "type": "object",
34
+ "children": merged,
35
+ "conflicts": conflicts,
36
+ },
37
+ }
38
+
39
+ # List of lists
40
+ if all(e["type"] == "list" for e in element_summaries):
41
+ # Merge list structures recursively
42
+ merged_list = merge_list_summaries(element_summaries)
43
+ return {
44
+ "type": "list",
45
+ "child": merged_list,
46
+ }
47
+
48
+ return {"type": "list", "child_type": "mixed"}
49
+ else:
50
+ return {"type": type(node).__name__}
51
+
52
+ def merge_object_summaries(summaries):
53
+ key_structures = {}
54
+ key_counts = Counter()
55
+ total = len(summaries)
56
+
57
+ for summary in summaries:
58
+ for key, val in summary.get("children", {}).items():
59
+ key_counts[key] += 1
60
+ if key not in key_structures:
61
+ key_structures[key] = []
62
+ key_structures[key].append(val)
63
+
64
+ merged = {}
65
+ conflicts = {}
66
+
67
+ for key, structures in key_structures.items():
68
+ count = key_counts[key]
69
+ optional = count < total
70
+
71
+ # Get unique types for this key
72
+ types = {s["type"] for s in structures}
73
+
74
+ if len(types) == 1:
75
+ # All structures have the same type
76
+ structure_type = list(types)[0]
77
+
78
+ if structure_type == "object":
79
+ # Recursively merge object structures
80
+ merged_children, child_conflicts = merge_object_summaries(
81
+ structures
82
+ )
83
+ merged[key] = {
84
+ "type": "object",
85
+ "children": merged_children,
86
+ "optional": optional,
87
+ }
88
+ if child_conflicts:
89
+ merged[key]["conflicts"] = child_conflicts
90
+
91
+ elif structure_type == "list":
92
+ # Merge list structures
93
+ merged_list = merge_list_summaries(structures)
94
+ merged[key] = {
95
+ "type": "list",
96
+ "optional": optional,
97
+ **{k: v for k, v in merged_list.items() if k != "type"},
98
+ }
99
+
100
+ else:
101
+ # Primitive type
102
+ merged[key] = {"type": structure_type, "optional": optional}
103
+ else:
104
+ # Type conflict
105
+ merged[key] = {"type": "conflict", "optional": optional}
106
+ conflicts[key] = list(types)
107
+
108
+ return merged, conflicts
109
+
110
+ def merge_list_summaries(summaries):
111
+ child_types = set()
112
+ child_structures = []
113
+
114
+ for summary in summaries:
115
+ if "child_type" in summary:
116
+ child_types.add(summary["child_type"])
117
+ elif "child" in summary:
118
+ child_structures.append(summary["child"])
119
+
120
+ if child_structures:
121
+ # All lists contain complex structures
122
+ structure_types = {s["type"] for s in child_structures}
123
+
124
+ if len(structure_types) == 1:
125
+ structure_type = list(structure_types)[0]
126
+
127
+ if structure_type == "object":
128
+ # Merge object structures within lists
129
+ merged_children, child_conflicts = merge_object_summaries(
130
+ child_structures
131
+ )
132
+ return {
133
+ "type": "list",
134
+ "child": {
135
+ "type": "object",
136
+ "children": merged_children,
137
+ "conflicts": child_conflicts,
138
+ },
139
+ }
140
+ elif structure_type == "list":
141
+ # Nested lists
142
+ merged_nested = merge_list_summaries(child_structures)
143
+ return {"type": "list", "child": merged_nested}
144
+
145
+ return {"type": "list", "child_type": "mixed_complex"}
146
+
147
+ elif child_types:
148
+ # Simple child types
149
+ if len(child_types) == 1:
150
+ return {"type": "list", "child_type": list(child_types)[0]}
151
+ else:
152
+ return {
153
+ "type": "list",
154
+ "child_type": f"mixed: {', '.join(sorted(child_types))}",
155
+ }
156
+
157
+ return {"type": "list", "child_type": "unknown"}
158
+
159
+ return analyze_node(data)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ from rich import print as rprint
164
+ from rich.pretty import pretty_repr
165
+
166
+ test_data = {
167
+ "users": [
168
+ {
169
+ "id": 1,
170
+ "name": "Alice",
171
+ "profile": {
172
+ "age": 30,
173
+ "settings": {"theme": "dark", "notifications": True},
174
+ },
175
+ "tags": ["admin", "user"],
176
+ },
177
+ {
178
+ "id": 2,
179
+ "name": "Bob",
180
+ "profile": {
181
+ "age": 25,
182
+ "settings": {
183
+ "theme": "light",
184
+ "notifications": False,
185
+ "language": "en",
186
+ },
187
+ },
188
+ "tags": ["user"],
189
+ "email": "bob@example.com",
190
+ },
191
+ ],
192
+ "metadata": {"total": 2, "created": "2024-01-01"},
193
+ }
194
+
195
+ summary = analyze_json(test_data)
196
+ rprint(pretty_repr(summary))
json_explorer/cli.py ADDED
@@ -0,0 +1,137 @@
1
+ from rich.console import Console
2
+
3
+ from .tree_view import print_json_analysis, print_compact_tree
4
+ from .search import JsonSearcher, SearchMode
5
+ from .stats import DataStatsAnalyzer
6
+ from .visualizer import JSONVisualizer
7
+ from .filter_parser import FilterExpressionParser
8
+
9
+
10
+ class CLIHandler:
11
+ """Handle command-line interface operations."""
12
+
13
+ def __init__(self):
14
+ self.data = None
15
+ self.source = None
16
+ self.console = Console()
17
+ self.searcher = JsonSearcher()
18
+ self.analyzer = DataStatsAnalyzer()
19
+ self.visualizer = JSONVisualizer()
20
+
21
+ def set_data(self, data, source):
22
+ """Set the data and source for processing."""
23
+ self.data = data
24
+ self.source = source
25
+
26
+ def run(self, args):
27
+ """Run CLI mode operations based on arguments."""
28
+ if not self.data:
29
+ self.console.print("❌ [red]No data loaded[/red]")
30
+ return 1
31
+
32
+ self.console.print(f"📄 Loaded: {self.source}")
33
+
34
+ # Tree operations
35
+ if args.tree:
36
+ self._handle_tree_display(args.tree)
37
+
38
+ # Search operations
39
+ if args.search:
40
+ self._handle_search(args)
41
+
42
+ # Statistics
43
+ if args.stats:
44
+ self._handle_stats(args)
45
+
46
+ # Visualization
47
+ if args.plot:
48
+ self._handle_visualization(args)
49
+
50
+ return 0
51
+
52
+ def _handle_tree_display(self, tree_type):
53
+ """Handle tree display operations."""
54
+ self.console.print(f"\n🌳 JSON Tree Structure ({tree_type.title()}):")
55
+
56
+ if tree_type == "raw":
57
+ print_json_analysis(self.data, self.source, show_raw=True)
58
+ elif tree_type == "analysis":
59
+ print_json_analysis(self.data, self.source)
60
+ elif tree_type == "compact":
61
+ print_compact_tree(self.data, self.source)
62
+
63
+ def _handle_search(self, args):
64
+ """Handle search operations."""
65
+ search_mode = SearchMode(args.search_mode)
66
+ search_term = args.search
67
+
68
+ if args.search_type == "pair":
69
+ self.console.print(
70
+ f"\n🔍 Searching for key-value pair: '{search_term}' = '{args.search_value}'"
71
+ )
72
+ else:
73
+ self.console.print(f"\n🔍 Searching ({args.search_type}): '{search_term}'")
74
+
75
+ # Perform search based on type
76
+ if args.search_type == "key":
77
+ results = self.searcher.search_keys(self.data, search_term, search_mode)
78
+ elif args.search_type == "value":
79
+ results = self.searcher.search_values(self.data, search_term, search_mode)
80
+ elif args.search_type == "pair":
81
+ if not args.search_value:
82
+ self.console.print(
83
+ "❌ [red]--search-value required for pair search[/red]"
84
+ )
85
+ return
86
+ results = self.searcher.search_key_value_pairs(
87
+ self.data, search_term, args.search_value, search_mode, search_mode
88
+ )
89
+ elif args.search_type == "filter":
90
+ try:
91
+ filter_func = FilterExpressionParser.parse_filter(search_term)
92
+ results = self.searcher.search_with_filter(self.data, filter_func)
93
+ except Exception as e:
94
+ self.console.print(f"❌ [red]Filter error: {e}[/red]")
95
+ return
96
+ else:
97
+ self.console.print(f"❌ [red]Unknown search type: {args.search_type}[/red]")
98
+ return
99
+
100
+ # Display results
101
+ if results:
102
+ # Check if tree results display is requested
103
+ show_tree = getattr(args, "tree_results", False)
104
+ self.searcher.print_results(results, show_tree=show_tree, mode=search_mode)
105
+
106
+ self.console.print(f"\n📊 Found {len(results)} result(s)")
107
+ else:
108
+ self.console.print("[yellow]No results found.[/yellow]")
109
+
110
+ def _handle_stats(self, args):
111
+ """Handle statistics display."""
112
+ self.console.print("\n📊 JSON Statistics:")
113
+ detailed = getattr(args, "detailed", False)
114
+ self.analyzer.print_summary(self.data, detailed=detailed)
115
+
116
+ def _handle_visualization(self, args):
117
+ """Handle visualization generation."""
118
+ self.console.print("\n📈 Generating visualizations...")
119
+
120
+ plot_format = getattr(args, "plot_format", "matplotlib")
121
+ save_path = getattr(args, "save_path", None)
122
+ detailed = getattr(args, "detailed", False)
123
+ open_browser = not getattr(args, "no_browser", False)
124
+
125
+ try:
126
+ self.visualizer.visualize(
127
+ self.data,
128
+ output=plot_format,
129
+ save_path=save_path,
130
+ detailed=detailed,
131
+ open_browser=open_browser,
132
+ )
133
+ self.console.print(
134
+ "✅ [green]Visualizations generated successfully[/green]"
135
+ )
136
+ except Exception as e:
137
+ self.console.print(f"❌ [red]Visualization error: {e}[/red]")
@@ -0,0 +1,132 @@
1
+ import ast
2
+ import operator
3
+
4
+
5
+ class FilterExpressionParser:
6
+ """Parse and evaluate filter expressions for search functionality."""
7
+
8
+ SAFE_OPERATORS = {
9
+ ast.Add: operator.add,
10
+ ast.Sub: operator.sub,
11
+ ast.Mult: operator.mul,
12
+ ast.Div: operator.truediv,
13
+ ast.Mod: operator.mod,
14
+ ast.Eq: operator.eq,
15
+ ast.NotEq: operator.ne,
16
+ ast.Lt: operator.lt,
17
+ ast.LtE: operator.le,
18
+ ast.Gt: operator.gt,
19
+ ast.GtE: operator.ge,
20
+ ast.In: lambda x, y: x in y,
21
+ ast.NotIn: lambda x, y: x not in y,
22
+ ast.Is: operator.is_,
23
+ ast.IsNot: operator.is_not,
24
+ ast.And: lambda x, y: x and y,
25
+ ast.Or: lambda x, y: x or y,
26
+ ast.Not: operator.not_,
27
+ }
28
+
29
+ @classmethod
30
+ def parse_filter(cls, expression):
31
+ """
32
+ Parse a filter expression and return a callable filter function.
33
+
34
+ Examples:
35
+ - "isinstance(value, int) and value > 10"
36
+ - "key.startswith('user')"
37
+ - "depth <= 3 and 'email' in str(value)"
38
+ - "len(str(value)) > 20"
39
+ """
40
+ try:
41
+ # Parse the expression into an AST
42
+ tree = ast.parse(expression, mode="eval")
43
+
44
+ def filter_func(key: str, value, depth):
45
+ env = {
46
+ "key": key,
47
+ "value": value,
48
+ "depth": depth,
49
+ "isinstance": isinstance,
50
+ "len": len,
51
+ "str": str,
52
+ "int": int,
53
+ "float": float,
54
+ "bool": bool,
55
+ "list": list,
56
+ "dict": dict,
57
+ "type": type,
58
+ "hasattr": hasattr,
59
+ "getattr": getattr,
60
+ "NoneType": type(None),
61
+ }
62
+
63
+ try:
64
+ return cls._eval_node(tree.body, env)
65
+ except Exception:
66
+ return False
67
+
68
+ return filter_func
69
+
70
+ except SyntaxError as e:
71
+ raise ValueError(f"Invalid filter expression syntax: {e}")
72
+
73
+ @classmethod
74
+ def _eval_node(cls, node, env):
75
+ """Safely evaluate an AST node."""
76
+ if isinstance(node, ast.Constant):
77
+ return node.value
78
+ elif isinstance(node, ast.Name):
79
+ if node.id in env:
80
+ return env[node.id]
81
+ raise NameError(f"Name '{node.id}' is not defined")
82
+ elif isinstance(node, ast.BinOp):
83
+ left = cls._eval_node(node.left, env)
84
+ right = cls._eval_node(node.right, env)
85
+ op_func = cls.SAFE_OPERATORS.get(type(node.op))
86
+ if op_func:
87
+ return op_func(left, right)
88
+ raise ValueError(f"Unsupported operator: {type(node.op)}")
89
+ elif isinstance(node, ast.UnaryOp):
90
+ operand = cls._eval_node(node.operand, env)
91
+ op_func = cls.SAFE_OPERATORS.get(type(node.op))
92
+ if op_func:
93
+ return op_func(operand)
94
+ raise ValueError(f"Unsupported unary operator: {type(node.op)}")
95
+ elif isinstance(node, ast.Compare):
96
+ left = cls._eval_node(node.left, env)
97
+ result = True
98
+ for i, (op, comparator) in enumerate(zip(node.ops, node.comparators)):
99
+ right = cls._eval_node(comparator, env)
100
+ op_func = cls.SAFE_OPERATORS.get(type(op))
101
+ if op_func:
102
+ result = result and op_func(left, right)
103
+ left = right
104
+ else:
105
+ raise ValueError(f"Unsupported comparison operator: {type(op)}")
106
+ return result
107
+ elif isinstance(node, ast.BoolOp):
108
+ values = [cls._eval_node(value, env) for value in node.values]
109
+ if isinstance(node.op, ast.And):
110
+ return all(values)
111
+ elif isinstance(node.op, ast.Or):
112
+ return any(values)
113
+ elif isinstance(node, ast.Call):
114
+ func_name = node.func.id if isinstance(node.func, ast.Name) else None
115
+ if func_name in env and callable(env[func_name]):
116
+ args = [cls._eval_node(arg, env) for arg in node.args]
117
+ kwargs = {kw.arg: cls._eval_node(kw.value, env) for kw in node.keywords}
118
+ return env[func_name](*args, **kwargs)
119
+ raise ValueError(f"Function '{func_name}' is not available")
120
+ elif isinstance(node, ast.Attribute):
121
+ obj = cls._eval_node(node.value, env)
122
+ return getattr(obj, node.attr)
123
+ elif isinstance(node, ast.Subscript):
124
+ obj = cls._eval_node(node.value, env)
125
+ key = cls._eval_node(node.slice, env)
126
+ return obj[key]
127
+ elif isinstance(node, ast.List):
128
+ return [cls._eval_node(item, env) for item in node.elts]
129
+ elif isinstance(node, ast.Tuple):
130
+ return tuple(cls._eval_node(item, env) for item in node.elts)
131
+
132
+ raise ValueError(f"Unsupported AST node type: {type(node)}")