gh-space-shooter 0.0.1__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.
- gh_space_shooter/__init__.py +19 -0
- gh_space_shooter/cli.py +177 -0
- gh_space_shooter/console_printer.py +65 -0
- gh_space_shooter/constants.py +11 -0
- gh_space_shooter/game/__init__.py +27 -0
- gh_space_shooter/game/animator.py +101 -0
- gh_space_shooter/game/drawables/__init__.py +17 -0
- gh_space_shooter/game/drawables/bullet.py +83 -0
- gh_space_shooter/game/drawables/drawable.py +29 -0
- gh_space_shooter/game/drawables/enemy.py +58 -0
- gh_space_shooter/game/drawables/explosion.py +72 -0
- gh_space_shooter/game/drawables/ship.py +107 -0
- gh_space_shooter/game/drawables/starfield.py +65 -0
- gh_space_shooter/game/game_state.py +80 -0
- gh_space_shooter/game/render_context.py +56 -0
- gh_space_shooter/game/renderer.py +44 -0
- gh_space_shooter/game/strategies/__init__.py +14 -0
- gh_space_shooter/game/strategies/base_strategy.py +43 -0
- gh_space_shooter/game/strategies/column_strategy.py +46 -0
- gh_space_shooter/game/strategies/random_strategy.py +61 -0
- gh_space_shooter/game/strategies/row_strategy.py +41 -0
- gh_space_shooter/github_client.py +172 -0
- gh_space_shooter/py.typed +0 -0
- gh_space_shooter-0.0.1.dist-info/METADATA +141 -0
- gh_space_shooter-0.0.1.dist-info/RECORD +28 -0
- gh_space_shooter-0.0.1.dist-info/WHEEL +4 -0
- gh_space_shooter-0.0.1.dist-info/entry_points.txt +2 -0
- gh_space_shooter-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""GitHub API client for fetching contribution graph data."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from .constants import NUM_WEEKS
|
|
10
|
+
|
|
11
|
+
# Load environment variables from .env file
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ContributionDay(TypedDict):
|
|
16
|
+
"""Represents a single day's contribution data."""
|
|
17
|
+
|
|
18
|
+
date: str
|
|
19
|
+
count: int
|
|
20
|
+
level: int # 0-4 intensity level
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContributionWeek(TypedDict):
|
|
24
|
+
"""Represents a week of contribution data."""
|
|
25
|
+
|
|
26
|
+
days: list[ContributionDay]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ContributionData(TypedDict):
|
|
30
|
+
"""Complete contribution graph data."""
|
|
31
|
+
|
|
32
|
+
username: str
|
|
33
|
+
total_contributions: int
|
|
34
|
+
weeks: list[ContributionWeek]
|
|
35
|
+
fetched_at: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GitHubAPIError(Exception):
|
|
39
|
+
"""Raised when GitHub API request fails."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class GitHubClient:
|
|
45
|
+
"""Client for interacting with GitHub's GraphQL API."""
|
|
46
|
+
|
|
47
|
+
GITHUB_API_URL = "https://api.github.com/graphql"
|
|
48
|
+
GET_CONTRIBUTION_GRAPH_QUERY = """
|
|
49
|
+
query($username: String!) {
|
|
50
|
+
user(login: $username) {
|
|
51
|
+
contributionsCollection {
|
|
52
|
+
contributionCalendar {
|
|
53
|
+
totalContributions
|
|
54
|
+
weeks {
|
|
55
|
+
contributionDays {
|
|
56
|
+
date
|
|
57
|
+
contributionCount
|
|
58
|
+
contributionLevel
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, token: str):
|
|
68
|
+
"""
|
|
69
|
+
Initialize GitHub client.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
token: GitHub personal access token (required).
|
|
73
|
+
"""
|
|
74
|
+
self.token = token
|
|
75
|
+
self.client = httpx.Client(
|
|
76
|
+
headers={
|
|
77
|
+
"Authorization": f"Bearer {self.token}",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
},
|
|
80
|
+
timeout=30.0,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __enter__(self):
|
|
84
|
+
"""Context manager entry."""
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
88
|
+
"""Context manager exit - close HTTP client."""
|
|
89
|
+
self.close()
|
|
90
|
+
|
|
91
|
+
def close(self):
|
|
92
|
+
self.client.close()
|
|
93
|
+
|
|
94
|
+
def get_contribution_graph(self, username: str) -> ContributionData:
|
|
95
|
+
"""
|
|
96
|
+
Fetch contribution graph for a GitHub user (last 52 weeks).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
username: GitHub username to fetch data for
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
ContributionData with user's contribution information
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
GitHubAPIError: If the API request fails
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
response = self.client.post(
|
|
110
|
+
self.GITHUB_API_URL,
|
|
111
|
+
json={
|
|
112
|
+
"query": self.GET_CONTRIBUTION_GRAPH_QUERY,
|
|
113
|
+
"variables": {"username": username}
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
except httpx.HTTPError as e:
|
|
118
|
+
raise GitHubAPIError(f"Failed to fetch data from GitHub API: {e}") from e
|
|
119
|
+
|
|
120
|
+
data = response.json()
|
|
121
|
+
|
|
122
|
+
# Check for GraphQL errors
|
|
123
|
+
if "errors" in data:
|
|
124
|
+
errors = data["errors"]
|
|
125
|
+
error_messages = [error.get("message", str(error)) for error in errors]
|
|
126
|
+
raise GitHubAPIError(f"GraphQL errors: {', '.join(error_messages)}")
|
|
127
|
+
|
|
128
|
+
# Check if user exists
|
|
129
|
+
if not data.get("data", {}).get("user"):
|
|
130
|
+
raise GitHubAPIError(f"User '{username}' not found")
|
|
131
|
+
|
|
132
|
+
# Extract contribution data
|
|
133
|
+
calendar = data["data"]["user"]["contributionsCollection"][
|
|
134
|
+
"contributionCalendar"
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
# Parse weeks and days
|
|
138
|
+
weeks: list[ContributionWeek] = []
|
|
139
|
+
for week_data in calendar["weeks"]:
|
|
140
|
+
days: list[ContributionDay] = []
|
|
141
|
+
for day_data in week_data["contributionDays"]:
|
|
142
|
+
days.append(
|
|
143
|
+
{
|
|
144
|
+
"date": day_data["date"],
|
|
145
|
+
"count": day_data["contributionCount"],
|
|
146
|
+
"level": self._contribution_level_to_int(
|
|
147
|
+
day_data["contributionLevel"]
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
weeks.append({"days": days})
|
|
152
|
+
|
|
153
|
+
# Always return exactly NUM_WEEKS (truncate if more)
|
|
154
|
+
weeks = weeks[-NUM_WEEKS:] if len(weeks) > NUM_WEEKS else weeks
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"username": username,
|
|
158
|
+
"total_contributions": calendar["totalContributions"],
|
|
159
|
+
"weeks": weeks,
|
|
160
|
+
"fetched_at": datetime.now().isoformat(),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
LEVEL_MAP = {
|
|
164
|
+
"NONE": 0,
|
|
165
|
+
"FIRST_QUARTILE": 1,
|
|
166
|
+
"SECOND_QUARTILE": 2,
|
|
167
|
+
"THIRD_QUARTILE": 3,
|
|
168
|
+
"FOURTH_QUARTILE": 4,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def _contribution_level_to_int(self, level: str) -> int:
|
|
172
|
+
return self.LEVEL_MAP.get(level, 0)
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gh-space-shooter
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A CLI tool that visualizes GitHub contribution graphs as gamified GIFs
|
|
5
|
+
Author-email: zane <czl970721@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pillow>=10.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: typer>=0.12.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# gh-space-shooter 🚀
|
|
16
|
+
|
|
17
|
+
Transform your GitHub contribution graph into an epic space shooter game!
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- 🚀 **Galaga-style space shooter** - Classic arcade gameplay with your contribution data
|
|
24
|
+
- 📊 **GitHub integration** - Fetches your last 52 weeks of contributions automatically
|
|
25
|
+
- 🎮 **Smart enemy AI** - Multiple attack strategies (columns, rows, random patterns)
|
|
26
|
+
- 💥 **Particle effects** - Explosions with randomized particles and smooth animations
|
|
27
|
+
- 🎨 **Polished graphics** - Rounded enemies, smooth ship design, starfield background
|
|
28
|
+
- 📈 **Contribution stats** - View your coding activity statistics
|
|
29
|
+
- 💾 **Export options** - Save both the GIF and raw JSON data
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### From PyPI (Recommended)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install gh-space-shooter
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### From Source
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Clone the repository
|
|
43
|
+
git clone https://github.com/yourusername/gh-space-shooter.git
|
|
44
|
+
cd gh-space-shooter
|
|
45
|
+
|
|
46
|
+
# Install with uv
|
|
47
|
+
uv sync
|
|
48
|
+
|
|
49
|
+
# Or with pip
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Setup
|
|
54
|
+
|
|
55
|
+
1. Create a GitHub Personal Access Token:
|
|
56
|
+
- Go to https://github.com/settings/tokens
|
|
57
|
+
- Click "Generate new token (classic)"
|
|
58
|
+
- Select scopes: `read:user`
|
|
59
|
+
- Copy the generated token
|
|
60
|
+
|
|
61
|
+
2. Set up your environment:
|
|
62
|
+
```bash
|
|
63
|
+
# Copy the example env file
|
|
64
|
+
touch .env
|
|
65
|
+
echo "GH_TOKEN=your_token_here" >> .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Alternatively, export the token directly:
|
|
69
|
+
```bash
|
|
70
|
+
export GH_TOKEN=your_token_here
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
### Generate Your Game GIF
|
|
76
|
+
|
|
77
|
+
Transform your GitHub contributions into an epic space shooter!
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Basic usage - generates username-gh-space-shooter.gif
|
|
81
|
+
gh-space-shooter <username>
|
|
82
|
+
|
|
83
|
+
# Examples
|
|
84
|
+
gh-space-shooter torvalds
|
|
85
|
+
gh-space-shooter octocat
|
|
86
|
+
|
|
87
|
+
# Specify custom output filename
|
|
88
|
+
gh-space-shooter torvalds --output my-epic-game.gif
|
|
89
|
+
gh-space-shooter torvalds -o my-game.gif
|
|
90
|
+
|
|
91
|
+
# Choose enemy attack strategy
|
|
92
|
+
gh-space-shooter torvalds --strategy column # Enemies attack in columns
|
|
93
|
+
gh-space-shooter torvalds --strategy row # Enemies attack in rows
|
|
94
|
+
gh-space-shooter torvalds -s random # Random chaos (default)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This creates an animated GIF showing:
|
|
98
|
+
- Your contribution graph as enemies (more contributions = stronger enemies)
|
|
99
|
+
- A Galaga-style spaceship battling through your coding history
|
|
100
|
+
- Enemy attack patterns based on your chosen strategy
|
|
101
|
+
- Smooth animations with randomized particle effects
|
|
102
|
+
- Your contribution stats displayed in the console
|
|
103
|
+
|
|
104
|
+
### Advanced Options
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Save raw contribution data to JSON
|
|
108
|
+
gh-space-shooter torvalds --raw-output data.json
|
|
109
|
+
|
|
110
|
+
# Load from previously saved JSON (saves API rate limits)
|
|
111
|
+
gh-space-shooter --raw-input data.json --output game.gif
|
|
112
|
+
|
|
113
|
+
# Combine options
|
|
114
|
+
gh-space-shooter torvalds -o game.gif -ro data.json -s column
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Data Format
|
|
118
|
+
|
|
119
|
+
When saved to JSON, the data includes:
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"username": "torvalds",
|
|
123
|
+
"total_contributions": 1234,
|
|
124
|
+
"weeks": [
|
|
125
|
+
{
|
|
126
|
+
"days": [
|
|
127
|
+
{
|
|
128
|
+
"date": "2024-01-01",
|
|
129
|
+
"count": 5,
|
|
130
|
+
"level": 2
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
"fetched_at": "2024-12-30T12:00:00"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
gh_space_shooter/__init__.py,sha256=jBFfHY3YC8-3m-eVPIo3CWoQLsy5Yvd0vcbHVbUDTWY,337
|
|
2
|
+
gh_space_shooter/cli.py,sha256=AHONWh6Kv9Y8D7OWsnOH6kOjN73aSCXF8Ats9jKJwkY,5310
|
|
3
|
+
gh_space_shooter/console_printer.py,sha256=opT5vITkPJ_9BCPNMDays6EtXez9m86YXY9pK5_Hdh8,2345
|
|
4
|
+
gh_space_shooter/constants.py,sha256=HR_FYewYnjQ8V5fJwMS2UhiYSS3l0iR5Y8bmaS3thQE,498
|
|
5
|
+
gh_space_shooter/github_client.py,sha256=Ugt4wi8RAvnYPO8fF7-MgugD5t91nxQUcEnCf-Hmc-8,4771
|
|
6
|
+
gh_space_shooter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
gh_space_shooter/game/__init__.py,sha256=bIc-S0hOiYqYBtdmd9_CedqYY07Il6XiV__TqcbtJNA,707
|
|
8
|
+
gh_space_shooter/game/animator.py,sha256=5nCY0xfRl6tifwixUlUlVXCQfDjhsR6HbQBKHHmYHH8,2998
|
|
9
|
+
gh_space_shooter/game/game_state.py,sha256=pvKc-FAapVXuFNb2hCGoOhJI1P8kWXkyCf_lruc-j50,2804
|
|
10
|
+
gh_space_shooter/game/render_context.py,sha256=TPJw9RmRB0_786W-O7nfB61UoDzDT_SlEVdaSlW750c,1805
|
|
11
|
+
gh_space_shooter/game/renderer.py,sha256=J13eUOWHkImtiNWGyvRqORM7ngg1Q7LBUgs4cWFl26g,1617
|
|
12
|
+
gh_space_shooter/game/drawables/__init__.py,sha256=lzo3O5cxahrYTyOQVrz7rQ3Prkaj8Z_dpzlt0UdurOo,306
|
|
13
|
+
gh_space_shooter/game/drawables/bullet.py,sha256=8DjqXI1Ab4IINi77szJuRc5Bqilb0gvofKgcQ9s0ONM,2920
|
|
14
|
+
gh_space_shooter/game/drawables/drawable.py,sha256=xIJWI5m-kZCbvCfgYF4jiqTvPFelViJ1gLilz7adJoM,738
|
|
15
|
+
gh_space_shooter/game/drawables/enemy.py,sha256=RCIE4Vn2tis0iJmJO_JUIxnomSgDSsfFhrcPPWmFLhE,1921
|
|
16
|
+
gh_space_shooter/game/drawables/explosion.py,sha256=Ek2CCjjifkLVSpaJ4Nv8ey1VhkXNK6HeKK3wB0fRI5g,2618
|
|
17
|
+
gh_space_shooter/game/drawables/ship.py,sha256=j389lXOUWvPoFZUNvVN-OEjhTtIlEkAVeEYqF7gDBIE,3216
|
|
18
|
+
gh_space_shooter/game/drawables/starfield.py,sha256=ON_cQ6Qfg30ocLHQEp1dceqputHmxJ7sf61o4uNF4Ng,2530
|
|
19
|
+
gh_space_shooter/game/strategies/__init__.py,sha256=a34i4FlNv-aQ-oEkT1ha7oJDBt7Cd7EFpbMWsYNHpW0,338
|
|
20
|
+
gh_space_shooter/game/strategies/base_strategy.py,sha256=IwPdthCWkgsFdUsMjQgifpSJv1bPRCCuUMF0o00y6pk,1152
|
|
21
|
+
gh_space_shooter/game/strategies/column_strategy.py,sha256=AQHXVTRe5BEjc4QHRL9QLtkkelTzGUGf2531POGtkG0,1728
|
|
22
|
+
gh_space_shooter/game/strategies/random_strategy.py,sha256=l0GKMkGJa_QEvNrN57R_KgWDBafGebP926fZJqpdghc,2215
|
|
23
|
+
gh_space_shooter/game/strategies/row_strategy.py,sha256=8izcioSGrVYGdQ__GrdfAQxnJt1BLhwNdumeuQiDhpg,1426
|
|
24
|
+
gh_space_shooter-0.0.1.dist-info/METADATA,sha256=YIu9i1FpQeGP4PCJvohXG_cs2_O9bFR9HMHhWTu0J84,3493
|
|
25
|
+
gh_space_shooter-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
gh_space_shooter-0.0.1.dist-info/entry_points.txt,sha256=SmK2ET5vz62eaMC4mhxmLJ1f_H9qSTXOvFOHNo-qwCk,62
|
|
27
|
+
gh_space_shooter-0.0.1.dist-info/licenses/LICENSE,sha256=teCrgzzcmjYCQ-RqXkDmICcHMN1AfaabrjZsW6O3KEk,1075
|
|
28
|
+
gh_space_shooter-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zane Chen
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|