diff --git a/starcli/__main__.py b/starcli/__main__.py index a6efc2d..1a5e120 100644 --- a/starcli/__main__.py +++ b/starcli/__main__.py @@ -66,8 +66,8 @@ @click.option( "--layout", "-L", - type=click.Choice(["list", "table", "grid"], case_sensitive=False), - help="The output format (list, table, or grid), default is list", + type=click.Choice(["list", "table", "grid","graph"], case_sensitive=False), + help="The output format (list, table, grid, or graph), default is list", ) @click.option( "--stars", @@ -127,6 +127,13 @@ default=False, hidden=True, ) +@click.option( + "--timeout", + "-T", + type=int, + default=10, + help="Request timeout in seconds, default: 10", +) @click.option("--debug", is_flag=True, default=False, help="Turn on debugging mode") def cli( lang, @@ -145,6 +152,7 @@ def cli( auth="", pager=False, nop=False, + timeout=10, ): """Find trending repos on GitHub""" if debug: @@ -201,11 +209,12 @@ def cli( not spoken_language and not date_range ): # if filtering by spoken language and date range not required tmp_repos = search( - lang, created, pushed, stars, topic, user, debug, order, auth + lang, created, pushed, stars, topic, user, debug, order, auth, + timeout ) else: tmp_repos = search_github_trending( - lang, spoken_language, order, stars, date_range, debug + lang, spoken_language, order, stars, date_range, debug ) if not tmp_repos: # if search() returned None diff --git a/starcli/layouts.py b/starcli/layouts.py index 7f78077..744917d 100644 --- a/starcli/layouts.py +++ b/starcli/layouts.py @@ -15,6 +15,9 @@ SYMBOL_MAP = {"stars": "★", "forks": "⎇"} +# Block characters for the graph bars +GRAPH_BLOCKS = "█" + def shorten_count(number): """Shortens number""" @@ -52,6 +55,65 @@ def format_date_range(date_range): .append(")") ) +def graph_layout(repos): + """Displays repositories in a graph format using rich""" + + # Convert formatted star/fork counts to integers for calculation + def parse_count(count_str): + if count_str == "-1": + return 0 + # Handle shortened counts like "90.3k" + if isinstance(count_str, str) and 'k' in count_str.lower(): + # Convert "90.3k" to 90300 + return int(float(count_str.lower().replace('k', '')) * 1000) + return int(count_str) + + # Find the maximum star count for scaling + max_stars = max(parse_count(repo["stargazers_count"]) for repo in repos) + max_forks = max(parse_count(repo["forks"]) for repo in repos) + + # Maximum width for the bar (in characters) + max_bar_width = 40 + + table = Table(show_header=False, box=None, padding=(0, 1)) + table.add_column("Repo", style="bold yellow", no_wrap=True) + table.add_column("Stats") + table.add_column("Bars", width=max_bar_width + 10) + + for repo in repos: + name = Text(repo["name"], overflow="fold") + name.stylize(f"yellow link {repo['html_url']}") + + stats = format_stats(repo["stargazers_count"], repo["forks"]) + date_range_col = format_date_range(repo.get("date_range")) + stats_text = Text(stats, style="italic blue") + if date_range_col: + stats_text.append("\n") + stats_text.append(date_range_col) + + # Create bar graphs for stars and forks + stars_count = parse_count(repo["stargazers_count"]) + forks_count = parse_count(repo["forks"]) + + # Scale the bars + stars_bar_width = int((stars_count / max_stars) * max_bar_width) if max_stars > 0 else 0 + forks_bar_width = int((forks_count / max_stars) * max_bar_width) if max_stars > 0 else 0 + + # Create the bars + stars_bar = Text(GRAPH_BLOCKS * stars_bar_width, style="bright_yellow") + forks_bar = Text(GRAPH_BLOCKS * forks_bar_width, style="bright_blue") + + # Add labels + bars = Text(f"{SYMBOL_MAP['stars']} ", style="bright_yellow") + bars.append(stars_bar) + bars.append(f" {repo['stargazers_count']}\n") + bars.append(f"{SYMBOL_MAP['forks']} ", style="bright_blue") + bars.append(forks_bar) + bars.append(f" {repo['forks']}") + + table.add_row(name, stats_text, bars) + + console.print(table) def list_layout(repos): """Display repositories in a list layout using rich""" @@ -207,5 +269,7 @@ def print_layout(*args, layout="list"): table_layout(*args) elif layout == "grid": grid_layout(*args) + elif layout == "graph": + graph_layout(*args) else: list_layout(*args) diff --git a/starcli/search.py b/starcli/search.py index a327e24..7bb0b46 100644 --- a/starcli/search.py +++ b/starcli/search.py @@ -38,8 +38,12 @@ "unsupported": "The request is not supported.", "unknown": "An unknown error occurred.", "valid": "The request returned successfully, but an unknown exception occurred.", + "timeout": "The request timed out. Try again or increase the timeout value with --timeout.", } +# Default timeout in seconds for requests +DEFAULT_TIMEOUT = 10 + FORMAT = "%(message)s" httpclient_logger = logging.getLogger("http.client") @@ -111,7 +115,7 @@ def get_date(date): def get_valid_request( - url: str, auth: t.Optional[str] = "" + url: str, auth: t.Optional[str] = "", timeout: int = DEFAULT_TIMEOUT ) -> t.Optional[requests.Response]: """GET an url with auth and handle a connection error""" while True: @@ -119,10 +123,13 @@ def get_valid_request( session = requests.Session() if auth: session.auth = (auth.split(":")[0], auth.split(":")[1]) - request = session.get(url) + request = session.get(url, timeout=timeout) except requests.exceptions.ConnectionError: secho("Internet connection error...", fg="bright_red") return + except requests.exceptions.Timeout: + secho(STATUS_ACTIONS["timeout"], fg="bright_yellow") + return if not request.status_code in (200, 202): handling_code = search_error(request.status_code) @@ -204,6 +211,7 @@ def search( debug=False, order="desc", auth="", + timeout=DEFAULT_TIMEOUT, ): """Return repositories searched from GitHub API""" date_format = "%Y-%m-%d" # date format in iso format @@ -252,7 +260,7 @@ def search( else: debug_logger(debug, "Auth: off") - request = get_valid_request(url, auth) + request = get_valid_request(url, auth, timeout) if request is None: return request diff --git a/tests/test_cli.py b/tests/test_cli.py index 9a93fdc..9af09e6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -332,3 +332,8 @@ def assertions( if not_in_stderr: for s in not_in_stderr: assert not s in result.stderr, f"{s} shouldn't be in stderr" + + def test_cli_graph_layout(self): + """Test cli when --layout graph is passed""" + result = self.cli_result("--layout", "graph", clear_cache=False) + self.assertions(result) diff --git a/tests/test_search.py b/tests/test_search.py index 2431e55..2ec109d 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -172,3 +172,8 @@ def test_search_multiple_language(): assert repo["full_name"].count("/") == 1 assert repo["full_name"] == f"{repo['owner']['login']}/{repo['name']}" assert repo["html_url"] == "https://github.com/" + repo["full_name"] + +def test_search_with_timeout(): + """Test search with custom timeout""" + repos = search(languages=["python"], timeout=20) + assert repos