gitradar 0.1.5__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.
- demo/api/index.py +14 -0
- demo/gitradar/__init__.py +22 -0
- demo/gitradar/cli.py +214 -0
- demo/gitradar/config.py +117 -0
- demo/gitradar/models.py +67 -0
- demo/gitradar/prompts/__init__.py +18 -0
- demo/gitradar/services/github.py +179 -0
- demo/gitradar/services/llm.py +342 -0
- demo/gitradar/utils/ui.py +196 -0
- demo/gitradar/web/__init__.py +7 -0
- demo/gitradar/web/app.py +173 -0
- examples/python_sdk_usage.py +48 -0
- gitradar/__init__.py +22 -0
- gitradar/cli.py +214 -0
- gitradar/config.py +117 -0
- gitradar/models.py +67 -0
- gitradar/prompts/__init__.py +18 -0
- gitradar/prompts/gap_analysis_system.j2 +36 -0
- gitradar/prompts/gap_analysis_user.j2 +21 -0
- gitradar/prompts/query_expansion_system.j2 +19 -0
- gitradar/prompts/query_expansion_user.j2 +1 -0
- gitradar/prompts/relevance_evaluation_system.j2 +22 -0
- gitradar/prompts/relevance_evaluation_user.j2 +14 -0
- gitradar/services/github.py +179 -0
- gitradar/services/llm.py +342 -0
- gitradar/utils/ui.py +196 -0
- gitradar/web/__init__.py +7 -0
- gitradar/web/app.py +173 -0
- gitradar/web/static/apple-touch-icon.png +0 -0
- gitradar/web/static/css/style.css +1430 -0
- gitradar/web/static/favicon-96x96.png +0 -0
- gitradar/web/static/favicon.ico +0 -0
- gitradar/web/static/favicon.svg +1 -0
- gitradar/web/static/js/app.js +1206 -0
- gitradar/web/static/site.webmanifest +21 -0
- gitradar/web/static/web-app-manifest-192x192.png +0 -0
- gitradar/web/static/web-app-manifest-512x512.png +0 -0
- gitradar/web/templates/index.html +521 -0
- gitradar-0.1.5.dist-info/METADATA +251 -0
- gitradar-0.1.5.dist-info/RECORD +51 -0
- gitradar-0.1.5.dist-info/WHEEL +5 -0
- gitradar-0.1.5.dist-info/entry_points.txt +2 -0
- gitradar-0.1.5.dist-info/licenses/LICENSE +21 -0
- gitradar-0.1.5.dist-info/top_level.txt +4 -0
- tests/conftest.py +27 -0
- tests/test_cli.py +29 -0
- tests/test_llm.py +163 -0
- tests/test_models.py +90 -0
- tests/test_prompts.py +58 -0
- tests/test_relevance.py +75 -0
- tests/test_web.py +81 -0
gitradar/utils/ui.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from rich.columns import Columns
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
from gitradar import __version__
|
|
9
|
+
from gitradar.models import ExpandedQueries, GapAnalysisReport, RepositoryInfo
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def display_banner() -> None:
|
|
15
|
+
"""Print stylish GitRadar terminal banner."""
|
|
16
|
+
banner_text = Text()
|
|
17
|
+
banner_text.append("📡 ", style="bold cyan")
|
|
18
|
+
banner_text.append("GitRadar", style="bold white on blue")
|
|
19
|
+
banner_text.append(" CLI Market & Gap Analysis Tool ", style="bold cyan")
|
|
20
|
+
banner_text.append(f"[v{__version__}]", style="dim italic white")
|
|
21
|
+
|
|
22
|
+
console.print()
|
|
23
|
+
console.print(Panel(banner_text, border_style="cyan", expand=False))
|
|
24
|
+
console.print()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def display_query_strategy(queries: ExpandedQueries) -> None:
|
|
28
|
+
"""Display the AI generated search query expansion strategy."""
|
|
29
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
30
|
+
table.add_column("Key", style="bold yellow")
|
|
31
|
+
table.add_column("Value", style="cyan")
|
|
32
|
+
|
|
33
|
+
table.add_row("🔍 Search Keywords:", ", ".join(queries.search_keywords))
|
|
34
|
+
if queries.github_topics:
|
|
35
|
+
table.add_row("🏷️ GitHub Topics:", ", ".join(f"#{t}" for t in queries.github_topics))
|
|
36
|
+
if queries.target_languages:
|
|
37
|
+
table.add_row("💻 Target Languages:", ", ".join(queries.target_languages))
|
|
38
|
+
table.add_row("💡 Search Strategy:", queries.search_explanation)
|
|
39
|
+
|
|
40
|
+
console.print(
|
|
41
|
+
Panel(
|
|
42
|
+
table,
|
|
43
|
+
title="[bold green]🧠 LLM Search Strategy Generated[/bold green]",
|
|
44
|
+
border_style="green",
|
|
45
|
+
expand=True,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
console.print()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def display_repo_table(repositories: List[RepositoryInfo], title: str = "Relevant GitHub Repositories Found") -> None:
|
|
52
|
+
"""Display repositories in a Rich styled table."""
|
|
53
|
+
if not repositories:
|
|
54
|
+
console.print("[bold red]No relevant repositories found.[/bold red]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
table = Table(
|
|
58
|
+
title=f"📊 {title} ({len(repositories)} Repos)",
|
|
59
|
+
header_style="bold magenta",
|
|
60
|
+
border_style="dim white",
|
|
61
|
+
expand=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
table.add_column("#", justify="center", style="dim", width=3)
|
|
65
|
+
table.add_column("Repository", style="bold cyan", no_wrap=True)
|
|
66
|
+
table.add_column("Relevance 🎯", justify="center", style="bold")
|
|
67
|
+
table.add_column("Stars ⭐", justify="right", style="bold yellow")
|
|
68
|
+
table.add_column("Forks 🍴", justify="right", style="dim cyan")
|
|
69
|
+
table.add_column("Language 💻", style="green")
|
|
70
|
+
table.add_column("Last Updated 📅", justify="center", style="dim")
|
|
71
|
+
table.add_column("Description & Match Fit", style="white")
|
|
72
|
+
|
|
73
|
+
for idx, repo in enumerate(repositories, 1):
|
|
74
|
+
desc = repo.description or ""
|
|
75
|
+
if len(desc) > 50:
|
|
76
|
+
desc = desc[:47] + "..."
|
|
77
|
+
|
|
78
|
+
if repo.relevance_reason:
|
|
79
|
+
desc += f"\n[dim italic]Fit: {repo.relevance_reason}[/dim italic]"
|
|
80
|
+
|
|
81
|
+
rel_badge = "N/A"
|
|
82
|
+
if repo.relevance_score is not None:
|
|
83
|
+
if repo.relevance_score >= 80:
|
|
84
|
+
rel_badge = f"[bold green]🎯 {repo.relevance_score}% Match[/bold green]"
|
|
85
|
+
elif repo.relevance_score >= 50:
|
|
86
|
+
rel_badge = f"[bold yellow]⚡ {repo.relevance_score}% Match[/bold yellow]"
|
|
87
|
+
else:
|
|
88
|
+
rel_badge = f"[bold red]⚠️ {repo.relevance_score}% Match[/bold red]"
|
|
89
|
+
|
|
90
|
+
table.add_row(
|
|
91
|
+
str(idx),
|
|
92
|
+
f"[{repo.html_url}]{repo.full_name}[/]",
|
|
93
|
+
rel_badge,
|
|
94
|
+
f"{repo.stars:,}",
|
|
95
|
+
f"{repo.forks:,}",
|
|
96
|
+
repo.language or "N/A",
|
|
97
|
+
repo.updated_at or "N/A",
|
|
98
|
+
desc,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
console.print(table)
|
|
102
|
+
console.print()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def display_gap_report(report: GapAnalysisReport) -> None:
|
|
106
|
+
"""Display comprehensive Market & Gap Analysis Report in terminal UI."""
|
|
107
|
+
console.print()
|
|
108
|
+
console.print("[bold cyan]════════════════════════════════════════════════════════════════════════════════[/bold cyan]")
|
|
109
|
+
console.print(" 🎯 [bold white on blue] MARKET & GAP ANALYSIS REPORT [/bold white on blue]")
|
|
110
|
+
console.print("[bold cyan]════════════════════════════════════════════════════════════════════════════════[/bold cyan]")
|
|
111
|
+
console.print()
|
|
112
|
+
|
|
113
|
+
# 1. Summary & Market Saturation Gauge
|
|
114
|
+
sat_color = "green" if report.market_saturation in ["Düşük", "Low"] else ("yellow" if report.market_saturation in ["Orta", "Moderate"] else "red")
|
|
115
|
+
|
|
116
|
+
summary_text = (
|
|
117
|
+
f"[bold white]Project Idea Summary:[/bold white] {report.idea_summary}\n\n"
|
|
118
|
+
f"[bold white]Market Saturation:[/bold white] [{sat_color}]{report.market_saturation}[/{sat_color}] "
|
|
119
|
+
f"(Saturation Score: [bold]{report.saturation_score}/100[/bold]) | "
|
|
120
|
+
f"[bold white]Opportunity Score:[/bold white] [bold green]{report.opportunity_score}/100 🚀[/bold green]\n\n"
|
|
121
|
+
f"[bold white]Market Summary:[/bold white]\n{report.market_summary}"
|
|
122
|
+
)
|
|
123
|
+
console.print(Panel(summary_text, title="[bold yellow]📌 Market Overview & Saturation Gauge[/bold yellow]", border_style=sat_color))
|
|
124
|
+
console.print()
|
|
125
|
+
|
|
126
|
+
# 2. Key Competitors
|
|
127
|
+
if report.top_competitors:
|
|
128
|
+
comp_table = Table(title="🏆 Top Competitors & Analysis", header_style="bold blue", expand=True)
|
|
129
|
+
comp_table.add_column("Competitor Repo", style="bold cyan", width=25)
|
|
130
|
+
comp_table.add_column("Key Strengths", style="green")
|
|
131
|
+
comp_table.add_column("Weaknesses & Gaps", style="red")
|
|
132
|
+
|
|
133
|
+
for comp in report.top_competitors:
|
|
134
|
+
strengths = "\n".join(f"• {s}" for s in comp.key_strengths)
|
|
135
|
+
weaknesses = "\n".join(f"• {w}" for w in comp.weaknesses_or_gaps)
|
|
136
|
+
comp_table.add_row(comp.repo_name, strengths, weaknesses)
|
|
137
|
+
|
|
138
|
+
console.print(comp_table)
|
|
139
|
+
console.print()
|
|
140
|
+
|
|
141
|
+
# 3. Unmet Needs & Differentiators
|
|
142
|
+
unmet_md = "### 🚨 Unmet Needs & Ecosystem Gaps\n"
|
|
143
|
+
for gap in report.unmet_needs:
|
|
144
|
+
unmet_md += f"- ❌ **{gap}**\n"
|
|
145
|
+
|
|
146
|
+
diff_md = "### 💎 Key Differentiators\n"
|
|
147
|
+
for diff in report.differentiators:
|
|
148
|
+
diff_md += f"- ✨ **{diff}**\n"
|
|
149
|
+
|
|
150
|
+
console.print(
|
|
151
|
+
Columns(
|
|
152
|
+
[
|
|
153
|
+
Panel(Markdown(unmet_md), border_style="red", title="[bold red]Unmet Needs (Gaps)[/bold red]", expand=True),
|
|
154
|
+
Panel(Markdown(diff_md), border_style="green", title="[bold green]Key Differentiators[/bold green]", expand=True),
|
|
155
|
+
],
|
|
156
|
+
equal=True,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
console.print()
|
|
160
|
+
|
|
161
|
+
# 4. Actionable Recommendations
|
|
162
|
+
rec_md = "### 💡 Strategic Developer Recommendations\n"
|
|
163
|
+
for idx, rec in enumerate(report.actionable_recommendations, 1):
|
|
164
|
+
rec_md += f"**{idx}.** {rec}\n"
|
|
165
|
+
|
|
166
|
+
console.print(Panel(Markdown(rec_md), title="[bold cyan]🛠️ Action Plan & Recommendations[/bold cyan]", border_style="cyan"))
|
|
167
|
+
console.print()
|
|
168
|
+
|
|
169
|
+
# 5. Technical Implementation Guide & Open Source Building Blocks
|
|
170
|
+
if report.implementation_guide:
|
|
171
|
+
guide = report.implementation_guide
|
|
172
|
+
guide_md = ""
|
|
173
|
+
if guide.recommended_tech_stack:
|
|
174
|
+
techs = ", ".join(f"`{t}`" for t in guide.recommended_tech_stack)
|
|
175
|
+
guide_md += f"**Recommended Tech Stack:** {techs}\n\n"
|
|
176
|
+
if guide.architecture_overview:
|
|
177
|
+
guide_md += f"**Architecture Overview:**\n{guide.architecture_overview}\n\n"
|
|
178
|
+
|
|
179
|
+
if guide.open_source_building_blocks:
|
|
180
|
+
guide_md += "### 📦 Recommended Open-Source Building Blocks\n"
|
|
181
|
+
for tool in guide.open_source_building_blocks:
|
|
182
|
+
link_str = f" ([link]({tool.repo_url}))" if tool.repo_url else ""
|
|
183
|
+
guide_md += f"- **{tool.name}** `[{tool.category}]`{link_str}: {tool.description_and_usage}\n"
|
|
184
|
+
|
|
185
|
+
console.print(Panel(Markdown(guide_md), title="[bold magenta]⚙️ Technical Implementation & Open-Source Roadmap[/bold magenta]", border_style="magenta"))
|
|
186
|
+
console.print()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def display_error(message: str) -> None:
|
|
190
|
+
"""Print an error panel."""
|
|
191
|
+
console.print(Panel(f"[bold red]ERROR:[/bold red] {message}", title="❌ Operation Failed", border_style="red"))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def display_info(message: str) -> None:
|
|
195
|
+
"""Print an info callout."""
|
|
196
|
+
console.print(f"[bold blue]ℹ️ {message}[/bold blue]")
|
gitradar/web/__init__.py
ADDED
gitradar/web/app.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import concurrent.futures
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from flask import Flask, render_template, request, jsonify, send_from_directory
|
|
5
|
+
from gitradar.config import settings
|
|
6
|
+
from gitradar.services.github import GitHubService
|
|
7
|
+
from gitradar.services.llm import LLMService
|
|
8
|
+
|
|
9
|
+
BASE_DIR = Path(__file__).parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def safe_run_async(coro):
|
|
13
|
+
"""Run an async coroutine safely, handling cases where an asyncio event loop is already running (e.g. Vercel / ASGI)."""
|
|
14
|
+
try:
|
|
15
|
+
loop = asyncio.get_running_loop()
|
|
16
|
+
except RuntimeError:
|
|
17
|
+
loop = None
|
|
18
|
+
|
|
19
|
+
if loop and loop.is_running():
|
|
20
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
21
|
+
return executor.submit(asyncio.run, coro).result()
|
|
22
|
+
else:
|
|
23
|
+
return asyncio.run(coro)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def create_app() -> Flask:
|
|
27
|
+
"""Create and configure the Flask web application instance."""
|
|
28
|
+
app = Flask(
|
|
29
|
+
__name__,
|
|
30
|
+
template_folder=str(BASE_DIR / "templates"),
|
|
31
|
+
static_folder=str(BASE_DIR / "static"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@app.route("/")
|
|
35
|
+
def index():
|
|
36
|
+
return render_template("index.html")
|
|
37
|
+
|
|
38
|
+
@app.route("/<path:filename>")
|
|
39
|
+
def serve_static_root(filename):
|
|
40
|
+
target = BASE_DIR / "static" / filename
|
|
41
|
+
if target.is_file():
|
|
42
|
+
return send_from_directory(BASE_DIR / "static", filename)
|
|
43
|
+
return jsonify({"error": "Not found"}), 404
|
|
44
|
+
|
|
45
|
+
@app.route("/api/config", methods=["GET"])
|
|
46
|
+
def get_config():
|
|
47
|
+
return jsonify({
|
|
48
|
+
"openai_configured": bool(settings.openai_api_key or settings.groq_api_key),
|
|
49
|
+
"groq_configured": bool(settings.groq_api_key),
|
|
50
|
+
"github_configured": bool(settings.github_token),
|
|
51
|
+
"default_model": settings.default_model,
|
|
52
|
+
"default_language": settings.default_language,
|
|
53
|
+
"max_repos_to_analyze": settings.max_repos_to_analyze,
|
|
54
|
+
"base_url": settings.openai_base_url,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
@app.route("/api/analyze", methods=["POST"])
|
|
58
|
+
def analyze_idea():
|
|
59
|
+
data = request.get_json() or {}
|
|
60
|
+
idea = data.get("idea", "").strip()
|
|
61
|
+
limit = int(data.get("limit", 10))
|
|
62
|
+
model = data.get("model") or settings.default_model
|
|
63
|
+
language = data.get("language") or settings.default_language
|
|
64
|
+
|
|
65
|
+
def clean_key(v):
|
|
66
|
+
if not v:
|
|
67
|
+
return None
|
|
68
|
+
cleaned = str(v).replace("\r", "").replace("\n", "").replace("\t", "").strip().strip("'\"")
|
|
69
|
+
return cleaned if cleaned else None
|
|
70
|
+
|
|
71
|
+
# Extract user credentials and OpenAI-compatible endpoint from headers or JSON payload
|
|
72
|
+
api_key = clean_key(
|
|
73
|
+
request.headers.get("X-OpenAI-Api-Key")
|
|
74
|
+
or request.headers.get("X-Groq-Api-Key")
|
|
75
|
+
or data.get("openai_key")
|
|
76
|
+
or data.get("openaiKey")
|
|
77
|
+
or data.get("api_key")
|
|
78
|
+
or data.get("groq_key")
|
|
79
|
+
or data.get("groqKey")
|
|
80
|
+
or settings.openai_api_key
|
|
81
|
+
or settings.groq_api_key
|
|
82
|
+
)
|
|
83
|
+
base_url = clean_key(
|
|
84
|
+
request.headers.get("X-OpenAI-Base-Url")
|
|
85
|
+
or data.get("base_url")
|
|
86
|
+
or data.get("baseUrl")
|
|
87
|
+
or settings.openai_base_url
|
|
88
|
+
)
|
|
89
|
+
github_token = clean_key(
|
|
90
|
+
request.headers.get("X-Github-Token")
|
|
91
|
+
or data.get("github_token")
|
|
92
|
+
or data.get("githubToken")
|
|
93
|
+
or settings.github_token
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if not idea:
|
|
97
|
+
return jsonify({"error": "Project idea parameter is required."}), 400
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
llm_service = LLMService(api_key=api_key, base_url=base_url, model=model, language=language)
|
|
101
|
+
github_service = GitHubService(token=github_token)
|
|
102
|
+
|
|
103
|
+
# 1. Expand Queries
|
|
104
|
+
queries = llm_service.expand_idea_to_queries(idea, language=language)
|
|
105
|
+
|
|
106
|
+
# 2. Search GitHub Repositories
|
|
107
|
+
raw_repos = safe_run_async(
|
|
108
|
+
github_service.search_and_enrich(
|
|
109
|
+
keywords=queries.search_keywords,
|
|
110
|
+
topics=queries.github_topics,
|
|
111
|
+
limit=max(limit * 2, 15),
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
min_relevance = int(data.get("min_relevance") or data.get("minRelevance") or settings.min_relevance_threshold)
|
|
117
|
+
except (ValueError, TypeError):
|
|
118
|
+
min_relevance = settings.min_relevance_threshold
|
|
119
|
+
|
|
120
|
+
# 3. Relevance Evaluation & Hybrid Ranking
|
|
121
|
+
eval_repos = llm_service.evaluate_repository_relevance(idea, raw_repos, language=language)
|
|
122
|
+
repos = github_service.rank_and_sort_by_relevance(eval_repos, limit=limit, min_relevance=min_relevance)
|
|
123
|
+
|
|
124
|
+
# 4. Perform Gap Analysis
|
|
125
|
+
report = llm_service.analyze_market_and_gaps(idea, repos, language=language)
|
|
126
|
+
|
|
127
|
+
return jsonify({
|
|
128
|
+
"queries": queries.model_dump(),
|
|
129
|
+
"repositories": [r.model_dump() for r in repos],
|
|
130
|
+
"report": report.model_dump(),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
except ValueError as ve:
|
|
134
|
+
return jsonify({"error": str(ve)}), 400
|
|
135
|
+
except Exception as e:
|
|
136
|
+
err_msg = str(e)
|
|
137
|
+
if any(k in err_msg.lower() for k in ["invalid_api_key", "invalid api key", "authentication", "unauthorized"]):
|
|
138
|
+
err_msg = "Invalid AI API Key! Please click the Settings Modal (⚙️) in the top-right corner and enter a valid API Key."
|
|
139
|
+
return jsonify({"error": err_msg}), 400
|
|
140
|
+
|
|
141
|
+
@app.route("/api/search", methods=["POST"])
|
|
142
|
+
def search_repos():
|
|
143
|
+
data = request.get_json() or {}
|
|
144
|
+
query = data.get("query", "").strip()
|
|
145
|
+
limit = int(data.get("limit", 10))
|
|
146
|
+
sort = data.get("sort", "stars")
|
|
147
|
+
|
|
148
|
+
def clean_key(v):
|
|
149
|
+
if not v:
|
|
150
|
+
return None
|
|
151
|
+
cleaned = str(v).replace("\r", "").replace("\n", "").replace("\t", "").strip().strip("'\"")
|
|
152
|
+
return cleaned if cleaned else None
|
|
153
|
+
|
|
154
|
+
github_token = clean_key(
|
|
155
|
+
request.headers.get("X-Github-Token")
|
|
156
|
+
or data.get("github_token")
|
|
157
|
+
or data.get("githubToken")
|
|
158
|
+
or settings.github_token
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if not query:
|
|
162
|
+
return jsonify({"error": "Search query parameter is required."}), 400
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
github_service = GitHubService(token=github_token)
|
|
166
|
+
repos = safe_run_async(github_service.search_repositories(query, limit=limit, sort=sort))
|
|
167
|
+
return jsonify({
|
|
168
|
+
"repositories": [r.model_dump() for r in repos]
|
|
169
|
+
})
|
|
170
|
+
except Exception as e:
|
|
171
|
+
return jsonify({"error": str(e)}), 500
|
|
172
|
+
|
|
173
|
+
return app
|
|
Binary file
|