synaptic-graph 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.
@@ -0,0 +1,12 @@
1
+ lib/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ *.html
11
+ *.svg
12
+ !assets/demo.svg
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: synaptic-graph
3
+ Version: 0.1.0
4
+ Summary: Visualize the dependency graph of any Python project — internal imports, cloud SDKs and HTTP calls.
5
+ Project-URL: Homepage, https://github.com/darkvius/synaptic
6
+ Project-URL: Repository, https://github.com/darkvius/synaptic
7
+ Project-URL: Issues, https://github.com/darkvius/synaptic/issues
8
+ License: MIT
9
+ Keywords: architecture,ast,dependencies,graph,visualization
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: graphviz>=0.20
22
+ Requires-Dist: networkx>=3.2
23
+ Requires-Dist: numpy>=1.24
24
+ Requires-Dist: pyvis>=0.3
25
+ Requires-Dist: rich>=13
26
+ Requires-Dist: textual>=0.80
27
+ Requires-Dist: typer>=0.12
28
+ Description-Content-Type: text/markdown
29
+
30
+ <div align="center">
31
+
32
+ <img src="assets/demo.svg" alt="synaptic demo" width="100%"/>
33
+
34
+ # synaptic
35
+
36
+ **Visualize the dependency graph of any Python project.**
37
+ Internal imports · Cloud SDKs · HTTP clients · Circular deps — all in one command.
38
+
39
+ [![Python](https://img.shields.io/badge/python-3.10%2B-4F8EF7?style=flat-square&logo=python&logoColor=white)](https://python.org)
40
+ [![License](https://img.shields.io/badge/license-MIT-2ecc71?style=flat-square)](LICENSE)
41
+ [![Built with Typer](https://img.shields.io/badge/CLI-Typer-E84393?style=flat-square)](https://typer.tiangolo.com)
42
+ [![Powered by Rich](https://img.shields.io/badge/output-Rich-FF9900?style=flat-square)](https://rich.readthedocs.io)
43
+
44
+ </div>
45
+
46
+ ---
47
+
48
+ ## Features
49
+
50
+ - **Static AST analysis** — no runtime execution needed
51
+ - **Internal imports** — maps every `import` and `from X import Y` across your codebase
52
+ - **Cloud SDK detection** — identifies AWS (`boto3`), GCP (`google.cloud`, `firebase_admin`) and Azure (`azure.*`) usage
53
+ - **HTTP client detection** — flags modules using `requests`, `httpx`, `aiohttp`, `urllib3` and more
54
+ - **Circular dependency highlighting** — broken cycles rendered in red
55
+ - **Two output formats** — interactive HTML (`pyvis`) or static SVG/PNG (`graphviz`)
56
+ - **Rich terminal output** — live progress, color-coded summary
57
+
58
+ ---
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ pip install synaptic
64
+ ```
65
+
66
+ Or from source:
67
+
68
+ ```bash
69
+ git clone https://github.com/your-username/synaptic
70
+ cd synaptic
71
+ pip install -e .
72
+ ```
73
+
74
+ > **Requirements:** Python 3.10+, `graphviz` binary installed on your system (`apt install graphviz` / `brew install graphviz`).
75
+
76
+ ---
77
+
78
+ ## Quick start
79
+
80
+ ```bash
81
+ # Interactive HTML graph (default)
82
+ synaptic scan ./my-project
83
+
84
+ # Custom output path
85
+ synaptic scan ./my-project --output architecture.html
86
+
87
+ # SVG with circular dependency highlighting
88
+ synaptic scan ./my-project --output graph.svg --circular
89
+
90
+ # Skip cloud and HTTP detection, filter stdlib
91
+ synaptic scan ./my-project --no-cloud --no-http --filter-stdlib
92
+
93
+ # Include test files in the scan
94
+ synaptic scan ./my-project --tests
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Options
100
+
101
+ | Flag | Default | Description |
102
+ |---|---|---|
103
+ | `--output`, `-o` | `synaptic_graph.html` | Output file (`.html` or `.svg`) |
104
+ | `--cloud / --no-cloud` | `on` | Detect AWS / GCP / Azure SDKs |
105
+ | `--http / --no-http` | `on` | Detect HTTP client libraries |
106
+ | `--tests / --no-tests` | `off` | Include test files |
107
+ | `--filter-stdlib / --no-filter-stdlib` | `on` | Exclude Python stdlib from graph |
108
+ | `--filter-external / --no-filter-external` | `off` | Exclude third-party packages |
109
+ | `--circular`, `-c` | `off` | Highlight circular dependencies in red |
110
+ | `--version`, `-v` | — | Show version and exit |
111
+
112
+ ---
113
+
114
+ ## Architecture
115
+
116
+ ```
117
+ synaptic/
118
+ ├── cli.py # Typer CLI + Rich output
119
+ ├── scanner.py # Recursive .py file discovery
120
+ ├── parser.py # AST-based import analysis
121
+ ├── cloud_detector.py # AWS / GCP / Azure SDK detection
122
+ ├── http_detector.py # HTTP client library detection
123
+ ├── graph.py # networkx graph + graphviz / pyvis rendering
124
+ └── utils.py # Shared helpers
125
+ ```
126
+
127
+ ---
128
+
129
+ ## Node types
130
+
131
+ | Color | Meaning |
132
+ |---|---|
133
+ | 🔵 Blue | Internal project module |
134
+ | 🟠 Orange | AWS / GCP / Azure SDK |
135
+ | 🩷 Pink | HTTP client (requests, httpx…) |
136
+ | ⚫ Grey | Stdlib / external package |
137
+ | 🔴 Red edge | Circular dependency |
138
+
139
+ ---
140
+
141
+ ## License
142
+
143
+ MIT © 2024
@@ -0,0 +1,114 @@
1
+ <div align="center">
2
+
3
+ <img src="assets/demo.svg" alt="synaptic demo" width="100%"/>
4
+
5
+ # synaptic
6
+
7
+ **Visualize the dependency graph of any Python project.**
8
+ Internal imports · Cloud SDKs · HTTP clients · Circular deps — all in one command.
9
+
10
+ [![Python](https://img.shields.io/badge/python-3.10%2B-4F8EF7?style=flat-square&logo=python&logoColor=white)](https://python.org)
11
+ [![License](https://img.shields.io/badge/license-MIT-2ecc71?style=flat-square)](LICENSE)
12
+ [![Built with Typer](https://img.shields.io/badge/CLI-Typer-E84393?style=flat-square)](https://typer.tiangolo.com)
13
+ [![Powered by Rich](https://img.shields.io/badge/output-Rich-FF9900?style=flat-square)](https://rich.readthedocs.io)
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## Features
20
+
21
+ - **Static AST analysis** — no runtime execution needed
22
+ - **Internal imports** — maps every `import` and `from X import Y` across your codebase
23
+ - **Cloud SDK detection** — identifies AWS (`boto3`), GCP (`google.cloud`, `firebase_admin`) and Azure (`azure.*`) usage
24
+ - **HTTP client detection** — flags modules using `requests`, `httpx`, `aiohttp`, `urllib3` and more
25
+ - **Circular dependency highlighting** — broken cycles rendered in red
26
+ - **Two output formats** — interactive HTML (`pyvis`) or static SVG/PNG (`graphviz`)
27
+ - **Rich terminal output** — live progress, color-coded summary
28
+
29
+ ---
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install synaptic
35
+ ```
36
+
37
+ Or from source:
38
+
39
+ ```bash
40
+ git clone https://github.com/your-username/synaptic
41
+ cd synaptic
42
+ pip install -e .
43
+ ```
44
+
45
+ > **Requirements:** Python 3.10+, `graphviz` binary installed on your system (`apt install graphviz` / `brew install graphviz`).
46
+
47
+ ---
48
+
49
+ ## Quick start
50
+
51
+ ```bash
52
+ # Interactive HTML graph (default)
53
+ synaptic scan ./my-project
54
+
55
+ # Custom output path
56
+ synaptic scan ./my-project --output architecture.html
57
+
58
+ # SVG with circular dependency highlighting
59
+ synaptic scan ./my-project --output graph.svg --circular
60
+
61
+ # Skip cloud and HTTP detection, filter stdlib
62
+ synaptic scan ./my-project --no-cloud --no-http --filter-stdlib
63
+
64
+ # Include test files in the scan
65
+ synaptic scan ./my-project --tests
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Options
71
+
72
+ | Flag | Default | Description |
73
+ |---|---|---|
74
+ | `--output`, `-o` | `synaptic_graph.html` | Output file (`.html` or `.svg`) |
75
+ | `--cloud / --no-cloud` | `on` | Detect AWS / GCP / Azure SDKs |
76
+ | `--http / --no-http` | `on` | Detect HTTP client libraries |
77
+ | `--tests / --no-tests` | `off` | Include test files |
78
+ | `--filter-stdlib / --no-filter-stdlib` | `on` | Exclude Python stdlib from graph |
79
+ | `--filter-external / --no-filter-external` | `off` | Exclude third-party packages |
80
+ | `--circular`, `-c` | `off` | Highlight circular dependencies in red |
81
+ | `--version`, `-v` | — | Show version and exit |
82
+
83
+ ---
84
+
85
+ ## Architecture
86
+
87
+ ```
88
+ synaptic/
89
+ ├── cli.py # Typer CLI + Rich output
90
+ ├── scanner.py # Recursive .py file discovery
91
+ ├── parser.py # AST-based import analysis
92
+ ├── cloud_detector.py # AWS / GCP / Azure SDK detection
93
+ ├── http_detector.py # HTTP client library detection
94
+ ├── graph.py # networkx graph + graphviz / pyvis rendering
95
+ └── utils.py # Shared helpers
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Node types
101
+
102
+ | Color | Meaning |
103
+ |---|---|
104
+ | 🔵 Blue | Internal project module |
105
+ | 🟠 Orange | AWS / GCP / Azure SDK |
106
+ | 🩷 Pink | HTTP client (requests, httpx…) |
107
+ | ⚫ Grey | Stdlib / external package |
108
+ | 🔴 Red edge | Circular dependency |
109
+
110
+ ---
111
+
112
+ ## License
113
+
114
+ MIT © 2024
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "synaptic-graph"
7
+ version = "0.1.0"
8
+ description = "Visualize the dependency graph of any Python project — internal imports, cloud SDKs and HTTP calls."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ keywords = ["dependencies", "graph", "architecture", "visualization", "ast"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "License :: OSI Approved :: MIT License",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ ]
25
+ dependencies = [
26
+ "typer>=0.12",
27
+ "rich>=13",
28
+ "graphviz>=0.20",
29
+ "pyvis>=0.3",
30
+ "networkx>=3.2",
31
+ "numpy>=1.24",
32
+ "textual>=0.80",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/darkvius/synaptic"
37
+ Repository = "https://github.com/darkvius/synaptic"
38
+ Issues = "https://github.com/darkvius/synaptic/issues"
39
+
40
+ [project.scripts]
41
+ synaptic = "synaptic.cli:app"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["synaptic"]
45
+
46
+ [tool.hatch.build.targets.sdist]
47
+ include = [
48
+ "synaptic/",
49
+ "README.md",
50
+ "pyproject.toml",
51
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,214 @@
1
+ """
2
+ cli.py — Synaptic CLI built with Typer + Rich.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import Optional
9
+ import sys
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.progress import Progress, SpinnerColumn, TextColumn
15
+ from rich import print as rprint
16
+
17
+ from synaptic import __version__
18
+
19
+ app = typer.Typer(
20
+ name="synaptic",
21
+ help="[bold cyan]synaptic[/] — visualize the dependency graph of any Python project.",
22
+ rich_markup_mode="rich",
23
+ add_completion=True,
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ def _version_callback(value: bool) -> None:
30
+ if value:
31
+ rprint(f"[bold cyan]synaptic[/] version [bold]{__version__}[/]")
32
+ raise typer.Exit()
33
+
34
+
35
+ @app.callback()
36
+ def main(
37
+ version: Optional[bool] = typer.Option(
38
+ None, "--version", "-v",
39
+ callback=_version_callback,
40
+ is_eager=True,
41
+ help="Show version and exit.",
42
+ ),
43
+ ) -> None:
44
+ pass
45
+
46
+
47
+ @app.command()
48
+ def scan(
49
+ project: Path = typer.Argument(
50
+ ...,
51
+ help="Root path of the Python project to analyse.",
52
+ exists=True,
53
+ file_okay=False,
54
+ dir_okay=True,
55
+ resolve_path=True,
56
+ ),
57
+ output: Path = typer.Option(
58
+ Path("synaptic_graph.html"),
59
+ "--output", "-o",
60
+ help="Output file path. Extension determines format: .html (interactive) or .svg.",
61
+ ),
62
+ cloud: bool = typer.Option(True, "--cloud/--no-cloud", help="Detect AWS / GCP / Azure SDK usage."),
63
+ http: bool = typer.Option(True, "--http/--no-http", help="Detect HTTP client library usage."),
64
+ tests: bool = typer.Option(False, "--tests/--no-tests", help="Include test files in the scan."),
65
+ filter_stdlib: bool = typer.Option(True, "--filter-stdlib/--no-filter-stdlib", help="Exclude Python stdlib modules from the graph."),
66
+ filter_external: bool = typer.Option(False, "--filter-external/--no-filter-external", help="Exclude third-party (non-project) modules from the graph."),
67
+ circular: bool = typer.Option(False, "--circular", "-c", help="Highlight circular dependencies in red."),
68
+ ) -> None:
69
+ """Scan *PROJECT* and generate a dependency graph."""
70
+
71
+ console.print(
72
+ Panel.fit(
73
+ f"[bold cyan]synaptic[/] [dim]v{__version__}[/]\n"
74
+ f"[dim]Scanning:[/] [bold]{project}[/]",
75
+ border_style="cyan",
76
+ )
77
+ )
78
+
79
+ with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
80
+
81
+ # 1. Scan files
82
+ task = progress.add_task("Scanning .py files...", total=None)
83
+ from synaptic.scanner import scan as do_scan
84
+ files = do_scan(project, include_tests=tests)
85
+ progress.update(task, description=f"[green]Found {len(files)} Python files[/]", completed=True)
86
+
87
+ if not files:
88
+ console.print("[yellow]No Python files found. Is the path correct?[/]")
89
+ raise typer.Exit(1)
90
+
91
+ # 2. Parse imports
92
+ task = progress.add_task("Parsing imports (AST)...", total=None)
93
+ from synaptic.parser import parse_project
94
+ edges = parse_project(files, project)
95
+ progress.update(task, description=f"[green]Parsed {len(edges)} import edges[/]", completed=True)
96
+
97
+ # 3. Cloud detection
98
+ cloud_deps = []
99
+ if cloud:
100
+ task = progress.add_task("Detecting cloud SDKs...", total=None)
101
+ from synaptic.cloud_detector import detect as detect_cloud
102
+ cloud_deps = detect_cloud(edges)
103
+ progress.update(task, description=f"[green]Found {len(cloud_deps)} cloud dependencies[/]", completed=True)
104
+
105
+ # 4. HTTP detection
106
+ http_deps = []
107
+ if http:
108
+ task = progress.add_task("Detecting HTTP clients...", total=None)
109
+ from synaptic.http_detector import detect as detect_http
110
+ http_deps = detect_http(edges)
111
+ progress.update(task, description=f"[green]Found {len(http_deps)} HTTP dependencies[/]", completed=True)
112
+
113
+ # 5. Build graph
114
+ task = progress.add_task("Building graph...", total=None)
115
+ from synaptic.utils import get_stdlib_modules, resolve_internal_modules, choose_output_format
116
+ from synaptic.graph import build, render_html, render_svg
117
+
118
+ internal_modules = resolve_internal_modules(files, project)
119
+ stdlib_modules = get_stdlib_modules()
120
+
121
+ G = build(
122
+ edges=edges,
123
+ cloud_deps=cloud_deps,
124
+ http_deps=http_deps,
125
+ internal_modules=internal_modules,
126
+ stdlib_modules=stdlib_modules,
127
+ filter_stdlib=filter_stdlib,
128
+ filter_external=filter_external,
129
+ highlight_circular=circular,
130
+ )
131
+ progress.update(task, description=f"[green]Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges[/]", completed=True)
132
+
133
+ # 6. Render
134
+ fmt = choose_output_format(output)
135
+ task = progress.add_task(f"Rendering {fmt.upper()}...", total=None)
136
+ if fmt == "html":
137
+ render_html(G, output)
138
+ else:
139
+ render_svg(G, output)
140
+ progress.update(task, description=f"[green]Saved to {output}[/]", completed=True)
141
+
142
+ # Summary
143
+ console.print()
144
+ console.print(
145
+ Panel(
146
+ f"[bold green]Done![/]\n\n"
147
+ f" [dim]Files scanned:[/] [bold]{len(files)}[/]\n"
148
+ f" [dim]Import edges:[/] [bold]{len(edges)}[/]\n"
149
+ f" [dim]Cloud deps:[/] [bold]{len(cloud_deps)}[/]\n"
150
+ f" [dim]HTTP deps:[/] [bold]{len(http_deps)}[/]\n"
151
+ f" [dim]Graph nodes:[/] [bold]{G.number_of_nodes()}[/]\n"
152
+ f" [dim]Graph edges:[/] [bold]{G.number_of_edges()}[/]\n\n"
153
+ f" [dim]Output:[/] [bold cyan]{output.resolve()}[/]",
154
+ title="[bold cyan]synaptic[/]",
155
+ border_style="green",
156
+ )
157
+ )
158
+
159
+
160
+ @app.command()
161
+ def tui(
162
+ project: Path = typer.Argument(
163
+ ...,
164
+ help="Root path of the Python project to analyse.",
165
+ exists=True,
166
+ file_okay=False,
167
+ dir_okay=True,
168
+ resolve_path=True,
169
+ ),
170
+ cloud: bool = typer.Option(True, "--cloud/--no-cloud", help="Detect AWS / GCP / Azure SDK usage."),
171
+ http: bool = typer.Option(True, "--http/--no-http", help="Detect HTTP client library usage."),
172
+ tests: bool = typer.Option(False, "--tests/--no-tests", help="Include test files in the scan."),
173
+ filter_stdlib: bool = typer.Option(True, "--filter-stdlib/--no-filter-stdlib", help="Exclude stdlib modules from the graph."),
174
+ filter_external: bool = typer.Option(False, "--filter-external/--no-filter-external", help="Exclude third-party modules."),
175
+ circular: bool = typer.Option(True, "--circular/--no-circular", help="Highlight circular dependencies."),
176
+ ) -> None:
177
+ """Launch the interactive TUI graph explorer for *PROJECT*."""
178
+ from synaptic.scanner import scan as do_scan
179
+ from synaptic.parser import parse_project
180
+ from synaptic.cloud_detector import detect as detect_cloud
181
+ from synaptic.http_detector import detect as detect_http
182
+ from synaptic.graph import build
183
+ from synaptic.utils import get_stdlib_modules, resolve_internal_modules
184
+ from synaptic.tui import launch
185
+
186
+ with console.status("[cyan]Building graph…[/]"):
187
+ files = do_scan(project, include_tests=tests)
188
+ if not files:
189
+ console.print("[yellow]No Python files found.[/]")
190
+ raise typer.Exit(1)
191
+
192
+ edges = parse_project(files, project)
193
+ cloud_deps = detect_cloud(edges) if cloud else []
194
+ http_deps = detect_http(edges) if http else []
195
+
196
+ internal_modules = resolve_internal_modules(files, project)
197
+ stdlib_modules = get_stdlib_modules()
198
+
199
+ G = build(
200
+ edges=edges,
201
+ cloud_deps=cloud_deps,
202
+ http_deps=http_deps,
203
+ internal_modules=internal_modules,
204
+ stdlib_modules=stdlib_modules,
205
+ filter_stdlib=filter_stdlib,
206
+ filter_external=filter_external,
207
+ highlight_circular=circular,
208
+ )
209
+
210
+ launch(G, project)
211
+
212
+
213
+ if __name__ == "__main__":
214
+ app()
@@ -0,0 +1,47 @@
1
+ """
2
+ cloud_detector.py — Identify cloud SDK usage from import edges.
3
+ """
4
+
5
+ from dataclasses import dataclass
6
+ from synaptic.parser import ImportEdge
7
+
8
+ # Map root package prefixes → (provider, service label)
9
+ CLOUD_SIGNATURES: dict[str, tuple[str, str]] = {
10
+ "boto3": ("AWS", "boto3"),
11
+ "botocore": ("AWS", "botocore"),
12
+ "aiobotocore": ("AWS", "aiobotocore"),
13
+ "google.cloud": ("GCP", "google-cloud"),
14
+ "google.api_core": ("GCP", "google-api-core"),
15
+ "firebase_admin": ("GCP", "firebase-admin"),
16
+ "googleapiclient": ("GCP", "google-api-python-client"),
17
+ "azure": ("Azure", "azure-sdk"),
18
+ "msrest": ("Azure", "msrest"),
19
+ }
20
+
21
+
22
+ @dataclass
23
+ class CloudDependency:
24
+ source: str # module that imports the SDK
25
+ provider: str # AWS | GCP | Azure
26
+ sdk: str # human-readable SDK name
27
+ raw_import: str # original import target string
28
+
29
+
30
+ def detect(edges: list[ImportEdge]) -> list[CloudDependency]:
31
+ """Return CloudDependency entries found in the import edge list."""
32
+ found: list[CloudDependency] = []
33
+
34
+ for edge in edges:
35
+ for prefix, (provider, sdk) in CLOUD_SIGNATURES.items():
36
+ if edge.target == prefix or edge.target.startswith(prefix + "."):
37
+ found.append(
38
+ CloudDependency(
39
+ source=edge.source,
40
+ provider=provider,
41
+ sdk=sdk,
42
+ raw_import=edge.target,
43
+ )
44
+ )
45
+ break # first match wins
46
+
47
+ return found