Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions starcli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -145,6 +152,7 @@ def cli(
auth="",
pager=False,
nop=False,
timeout=10,
):
"""Find trending repos on GitHub"""
if debug:
Expand Down Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions starcli/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

SYMBOL_MAP = {"stars": "★", "forks": "⎇"}

# Block characters for the graph bars
GRAPH_BLOCKS = "█"


def shorten_count(number):
"""Shortens number"""
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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)
14 changes: 11 additions & 3 deletions starcli/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -111,18 +115,21 @@ 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:
try:
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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
5 changes: 5 additions & 0 deletions tests/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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