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.
Files changed (51) hide show
  1. demo/api/index.py +14 -0
  2. demo/gitradar/__init__.py +22 -0
  3. demo/gitradar/cli.py +214 -0
  4. demo/gitradar/config.py +117 -0
  5. demo/gitradar/models.py +67 -0
  6. demo/gitradar/prompts/__init__.py +18 -0
  7. demo/gitradar/services/github.py +179 -0
  8. demo/gitradar/services/llm.py +342 -0
  9. demo/gitradar/utils/ui.py +196 -0
  10. demo/gitradar/web/__init__.py +7 -0
  11. demo/gitradar/web/app.py +173 -0
  12. examples/python_sdk_usage.py +48 -0
  13. gitradar/__init__.py +22 -0
  14. gitradar/cli.py +214 -0
  15. gitradar/config.py +117 -0
  16. gitradar/models.py +67 -0
  17. gitradar/prompts/__init__.py +18 -0
  18. gitradar/prompts/gap_analysis_system.j2 +36 -0
  19. gitradar/prompts/gap_analysis_user.j2 +21 -0
  20. gitradar/prompts/query_expansion_system.j2 +19 -0
  21. gitradar/prompts/query_expansion_user.j2 +1 -0
  22. gitradar/prompts/relevance_evaluation_system.j2 +22 -0
  23. gitradar/prompts/relevance_evaluation_user.j2 +14 -0
  24. gitradar/services/github.py +179 -0
  25. gitradar/services/llm.py +342 -0
  26. gitradar/utils/ui.py +196 -0
  27. gitradar/web/__init__.py +7 -0
  28. gitradar/web/app.py +173 -0
  29. gitradar/web/static/apple-touch-icon.png +0 -0
  30. gitradar/web/static/css/style.css +1430 -0
  31. gitradar/web/static/favicon-96x96.png +0 -0
  32. gitradar/web/static/favicon.ico +0 -0
  33. gitradar/web/static/favicon.svg +1 -0
  34. gitradar/web/static/js/app.js +1206 -0
  35. gitradar/web/static/site.webmanifest +21 -0
  36. gitradar/web/static/web-app-manifest-192x192.png +0 -0
  37. gitradar/web/static/web-app-manifest-512x512.png +0 -0
  38. gitradar/web/templates/index.html +521 -0
  39. gitradar-0.1.5.dist-info/METADATA +251 -0
  40. gitradar-0.1.5.dist-info/RECORD +51 -0
  41. gitradar-0.1.5.dist-info/WHEEL +5 -0
  42. gitradar-0.1.5.dist-info/entry_points.txt +2 -0
  43. gitradar-0.1.5.dist-info/licenses/LICENSE +21 -0
  44. gitradar-0.1.5.dist-info/top_level.txt +4 -0
  45. tests/conftest.py +27 -0
  46. tests/test_cli.py +29 -0
  47. tests/test_llm.py +163 -0
  48. tests/test_models.py +90 -0
  49. tests/test_prompts.py +58 -0
  50. tests/test_relevance.py +75 -0
  51. 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]")
@@ -0,0 +1,7 @@
1
+ """
2
+ GitRadar Web Dashboard Module.
3
+ """
4
+
5
+ from .app import create_app
6
+
7
+ __all__ = ["create_app"]
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