Skip to content

Commit 3554e0e

Browse files
committed
Create GitHub API radar
0 parents  commit 3554e0e

6 files changed

Lines changed: 235 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
python:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-python@v5
13+
with:
14+
python-version: "3.12"
15+
- name: Compile
16+
run: python -m py_compile radar.py
17+
- name: Smoke test
18+
run: python radar.py octocat

DEVELOPER_PROGRAM.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# GitHub Developer Program Notes
2+
3+
This repository is a public GitHub API integration in development.
4+
5+
## Integration
6+
7+
- Product name: Myshkin GitHub Radar
8+
- GitHub API surface: REST API
9+
- Primary endpoint examples:
10+
- `GET /users/{username}`
11+
- `GET /users/{username}/repos`
12+
- Current mode: command-line tool
13+
- Support channel: repository issues
14+
15+
## Application Checklist
16+
17+
- [x] Public integration repository exists.
18+
- [x] GitHub API calls are implemented.
19+
- [x] Support path is documented.
20+
- [ ] Add a public support email or contact mailbox.
21+
- [ ] Submit GitHub Developer Program registration.
22+
23+
The remaining items require account-level contact details and should be
24+
confirmed manually before submission.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Myshkin451
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Myshkin GitHub Radar
2+
3+
[![CI](https://github.com/myshkin451/myshkin-github-radar/actions/workflows/ci.yml/badge.svg)](https://github.com/myshkin451/myshkin-github-radar/actions/workflows/ci.yml)
4+
![GitHub API](https://img.shields.io/badge/GitHub%20API-REST-24292f?logo=github)
5+
![Status](https://img.shields.io/badge/status-development-6f42c1)
6+
![License](https://img.shields.io/badge/license-MIT-2ea44f)
7+
8+
A small command-line radar for GitHub profile, repository, and contribution
9+
signals.
10+
11+
The project exists for two practical reasons:
12+
13+
- to give me a real GitHub API integration to maintain in public;
14+
- to turn GitHub profile work into observable data instead of vibes.
15+
16+
## What It Does
17+
18+
`radar.py` calls the GitHub REST API and prints a compact Markdown report for a
19+
user account:
20+
21+
- profile basics;
22+
- public repository count;
23+
- recent public repository signals;
24+
- topic and language hints;
25+
- profile links that may need attention.
26+
27+
It can run anonymously for public data. If `GITHUB_TOKEN` is set, it uses the
28+
token for higher API limits and private-accessible data.
29+
30+
## Usage
31+
32+
```bash
33+
python3 radar.py myshkin451
34+
```
35+
36+
With a token:
37+
38+
```bash
39+
GITHUB_TOKEN=ghp_xxx python3 radar.py myshkin451
40+
```
41+
42+
## Why This Exists
43+
44+
I am interested in agents, knowledge systems, and personal publishing. GitHub is
45+
one of the places where those threads become visible: issues, pull requests,
46+
reviews, discussions, topics, and the small public traces that make future work
47+
easier to understand.
48+
49+
This repository is also the public integration record for a GitHub Developer
50+
Program application.
51+
52+
## Support
53+
54+
For support, open an issue in this repository.

SUPPORT.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Support
2+
3+
Open an issue in this repository for support, bug reports, or feature requests.
4+
5+
For the GitHub Developer Program application, this repository is the public
6+
support channel while the project is in development.

radar.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python3
2+
"""Small GitHub API radar for public profile signals."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
import sys
9+
import urllib.error
10+
import urllib.parse
11+
import urllib.request
12+
from collections import Counter
13+
from datetime import datetime, timezone
14+
15+
16+
API_ROOT = "https://api.github.com"
17+
18+
19+
def github_get(path: str):
20+
token = os.environ.get("GITHUB_TOKEN")
21+
request = urllib.request.Request(f"{API_ROOT}{path}")
22+
request.add_header("Accept", "application/vnd.github+json")
23+
request.add_header("X-GitHub-Api-Version", "2022-11-28")
24+
if token:
25+
request.add_header("Authorization", f"Bearer {token}")
26+
27+
try:
28+
with urllib.request.urlopen(request, timeout=30) as response:
29+
payload = response.read().decode("utf-8")
30+
return json.loads(payload)
31+
except urllib.error.HTTPError as exc:
32+
detail = exc.read().decode("utf-8", errors="replace")
33+
raise SystemExit(f"GitHub API error {exc.code}: {detail}") from exc
34+
35+
36+
def list_public_repos(username: str) -> list[dict]:
37+
repos: list[dict] = []
38+
page = 1
39+
while page <= 4:
40+
query = urllib.parse.urlencode(
41+
{
42+
"per_page": 100,
43+
"page": page,
44+
"sort": "updated",
45+
"direction": "desc",
46+
}
47+
)
48+
batch = github_get(f"/users/{username}/repos?{query}")
49+
if not batch:
50+
break
51+
repos.extend(batch)
52+
if len(batch) < 100:
53+
break
54+
page += 1
55+
return repos
56+
57+
58+
def render(username: str) -> str:
59+
user = github_get(f"/users/{username}")
60+
repos = list_public_repos(username)
61+
languages = Counter(repo.get("language") for repo in repos if repo.get("language"))
62+
recent = sorted(repos, key=lambda repo: repo.get("pushed_at") or "", reverse=True)[:8]
63+
total_stars = sum(repo.get("stargazers_count", 0) for repo in repos)
64+
65+
lines = [
66+
f"# GitHub Radar: {username}",
67+
"",
68+
f"Generated: {datetime.now(timezone.utc).isoformat(timespec='seconds')}",
69+
"",
70+
"## Profile",
71+
"",
72+
f"- Name: {user.get('name') or username}",
73+
f"- Bio: {user.get('bio') or '(empty)'}",
74+
f"- Blog: {user.get('blog') or '(empty)'}",
75+
f"- Public repos: {user.get('public_repos')}",
76+
f"- Followers: {user.get('followers')}",
77+
f"- Following: {user.get('following')}",
78+
f"- Total public stars: {total_stars}",
79+
"",
80+
"## Language Signals",
81+
"",
82+
]
83+
84+
if languages:
85+
for language, count in languages.most_common(8):
86+
lines.append(f"- {language}: {count}")
87+
else:
88+
lines.append("- No language metadata found.")
89+
90+
lines.extend(["", "## Recent Public Repositories", ""])
91+
if recent:
92+
for repo in recent:
93+
description = repo.get("description") or "(no description)"
94+
lines.append(
95+
f"- [{repo['name']}]({repo['html_url']}): {description} "
96+
f"({repo.get('language') or 'unknown'}, "
97+
f"{repo.get('stargazers_count', 0)} stars)"
98+
)
99+
else:
100+
lines.append("- No repositories found.")
101+
102+
return "\n".join(lines) + "\n"
103+
104+
105+
def main(argv: list[str]) -> int:
106+
username = argv[1] if len(argv) > 1 else "myshkin451"
107+
print(render(username))
108+
return 0
109+
110+
111+
if __name__ == "__main__":
112+
raise SystemExit(main(sys.argv))

0 commit comments

Comments
 (0)