spacecrew 1.1.0__tar.gz

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.
@@ -0,0 +1,28 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ name: Build package
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: astral-sh/setup-uv@v5
20
+
21
+ - name: Build
22
+ run: uv build
23
+
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+ if-no-files-found: error
@@ -0,0 +1,44 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ name: Publish to PyPI
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: astral-sh/setup-uv@v5
18
+
19
+ - name: Verify tag matches pyproject version
20
+ run: |
21
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
22
+ PYPROJECT_VERSION="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
23
+ if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
24
+ echo "::error::tag version 'v$TAG_VERSION' does not match pyproject version '$PYPROJECT_VERSION'"
25
+ exit 1
26
+ fi
27
+ echo "version $TAG_VERSION matches"
28
+
29
+ - name: Build
30
+ run: uv build
31
+
32
+ - name: Check version does not already exist on PyPI
33
+ run: |
34
+ VERSION="${GITHUB_REF_NAME#v}"
35
+ if curl -fsSI "https://pypi.org/pypi/spacecrew/$VERSION/json" > /dev/null 2>&1; then
36
+ echo "::error::spacecrew $VERSION already exists on PyPI"
37
+ exit 1
38
+ fi
39
+ echo "spacecrew $VERSION not found on PyPI, proceeding"
40
+
41
+ - name: Publish
42
+ env:
43
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
44
+ run: uv publish
@@ -0,0 +1,65 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+ MANIFEST
27
+
28
+ # PyInstaller
29
+ *.manifest
30
+ *.spec
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+ pip-delete-this-directory.txt
35
+
36
+ # Unit test / coverage reports
37
+ htmlcov/
38
+ .tox/
39
+ .nox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+ *.cover
46
+ .hypothesis/
47
+ .pytest_cache/
48
+
49
+ # Environments
50
+ .env
51
+ .venv
52
+ env/
53
+ venv/
54
+ ENV/
55
+ env.bak/
56
+ venv.bak/
57
+
58
+ # IDE
59
+ .idea/
60
+ .vscode/
61
+ *.swp
62
+ *.swo
63
+
64
+ # mypy
65
+ .mypy_cache/
@@ -0,0 +1,21 @@
1
+ default_install_hook_types:
2
+ - commit-msg
3
+ - pre-commit
4
+ repos:
5
+ - repo: local
6
+ hooks:
7
+ - id: needs-prefix
8
+ name: commit message needs to have a prefix
9
+ language: pygrep
10
+ entry: '^(chore|ci|junk|test|feat|fix|build|docs|refactor)!?(\([a-z]+\))?: (?![A-Z])(?![a-z]+(ed|ing)\s).*$'
11
+ args:
12
+ - '--multiline'
13
+ - '--negate'
14
+ stages:
15
+ - commit-msg
16
+ - repo: https://github.com/pre-commit/pre-commit-hooks
17
+ rev: v6.0.0
18
+ hooks:
19
+ - id: trailing-whitespace
20
+ - id: end-of-file-fixer
21
+ - id: check-added-large-files
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub Ginał
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.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: spacecrew
3
+ Version: 1.1.0
4
+ Summary: Check who is in space right now
5
+ Project-URL: Homepage, https://github.com/jakub-ginal/spacecrew
6
+ Author: Jakub Ginał
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: chafa-py>=1.2
11
+ Requires-Dist: pillow>=10.0
12
+ Requires-Dist: requests>=2.0
13
+ Requires-Dist: rich>=13.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # spacecrew
17
+ Spacecrew is a lightweight Python terminal program that shows who is in space right now.
18
+
19
+ <img width="800" height="600" alt="92_1x_shots_so" src="https://github.com/user-attachments/assets/dfd95ad7-904d-4495-9c98-b8e1751ea5f3" />
20
+
21
+
22
+ ## Tested Systems
23
+
24
+ * **CachyOS**
25
+ * **Ubuntu**
26
+ * **Debian**
27
+
28
+ ## Installation
29
+
30
+ Clone the repository and run the setup script to automatically install dependencies and configure the command:
31
+
32
+ ```bash
33
+ git clone https://github.com/jakub-ginal/spacecrew
34
+ cd spacecrew
35
+ chmod +x setup.sh
36
+ ./setup.sh
37
+ ```
38
+ Or run it via this single command:
39
+ ```bash
40
+ git clone https://github.com/jakub-ginal/spacecrew && cd spacecrew && bash setup.sh && hash -r && cd ~ && rm -rf ~/spacecrew
41
+ ```
42
+ ## Usage
43
+
44
+ Simply type `spacecrew` anywhere in your terminal.
45
+ Follow the interactive menu to select a mission or view specific astronaut details and photos.
46
+
47
+ ### Time in Space
48
+
49
+ * **p (previous)**: Days from past missions.
50
+ * **c (current)**: Days from the current mission (tracked live).
51
+ <img width="800" height="600" alt="356_1x_shots_so" src="https://github.com/user-attachments/assets/a87a2e9d-3c5a-443c-b62f-ec19f2922f0d" />
52
+
53
+
54
+ ## Uninstall
55
+
56
+ To completely remove the program and its configuration from your system, run:
57
+ ```bash
58
+ cd ~ && rm -rf ~/spacecrew && rm -f ~/.local/bin/spacecrew && hash -r
59
+ ```
60
+ ## Data Source
61
+
62
+ Data provided by [International Space Station APIs](https://github.com/corquaid/international-space-station-APIs) by **Cormac Quaid**.
@@ -0,0 +1,47 @@
1
+ # spacecrew
2
+ Spacecrew is a lightweight Python terminal program that shows who is in space right now.
3
+
4
+ <img width="800" height="600" alt="92_1x_shots_so" src="https://github.com/user-attachments/assets/dfd95ad7-904d-4495-9c98-b8e1751ea5f3" />
5
+
6
+
7
+ ## Tested Systems
8
+
9
+ * **CachyOS**
10
+ * **Ubuntu**
11
+ * **Debian**
12
+
13
+ ## Installation
14
+
15
+ Clone the repository and run the setup script to automatically install dependencies and configure the command:
16
+
17
+ ```bash
18
+ git clone https://github.com/jakub-ginal/spacecrew
19
+ cd spacecrew
20
+ chmod +x setup.sh
21
+ ./setup.sh
22
+ ```
23
+ Or run it via this single command:
24
+ ```bash
25
+ git clone https://github.com/jakub-ginal/spacecrew && cd spacecrew && bash setup.sh && hash -r && cd ~ && rm -rf ~/spacecrew
26
+ ```
27
+ ## Usage
28
+
29
+ Simply type `spacecrew` anywhere in your terminal.
30
+ Follow the interactive menu to select a mission or view specific astronaut details and photos.
31
+
32
+ ### Time in Space
33
+
34
+ * **p (previous)**: Days from past missions.
35
+ * **c (current)**: Days from the current mission (tracked live).
36
+ <img width="800" height="600" alt="356_1x_shots_so" src="https://github.com/user-attachments/assets/a87a2e9d-3c5a-443c-b62f-ec19f2922f0d" />
37
+
38
+
39
+ ## Uninstall
40
+
41
+ To completely remove the program and its configuration from your system, run:
42
+ ```bash
43
+ cd ~ && rm -rf ~/spacecrew && rm -f ~/.local/bin/spacecrew && hash -r
44
+ ```
45
+ ## Data Source
46
+
47
+ Data provided by [International Space Station APIs](https://github.com/corquaid/international-space-station-APIs) by **Cormac Quaid**.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "spacecrew"
3
+ version = "1.1.0"
4
+ description = "Check who is in space right now"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [{ name = "Jakub Ginał" }]
9
+ dependencies = [
10
+ "rich>=13.0",
11
+ "requests>=2.0",
12
+ "chafa.py>=1.2",
13
+ "pillow>=10.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/jakub-ginal/spacecrew"
18
+
19
+ [project.scripts]
20
+ spacecrew = "spacecrew:main"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,219 @@
1
+ import os
2
+ import time
3
+ from collections import defaultdict
4
+ from datetime import datetime
5
+ from io import BytesIO
6
+
7
+ import chafa
8
+ import requests
9
+ from PIL import Image
10
+ from rich.columns import Columns
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+ from rich.tree import Tree
16
+
17
+ console = Console()
18
+
19
+
20
+ def clear_screen():
21
+ os.system("clear")
22
+
23
+
24
+ def fetch_space_data():
25
+ url = "https://corquaid.github.io/international-space-station-APIs/JSON/people-in-space.json"
26
+ headers = {"User-Agent": "Mozilla/5.0"}
27
+ try:
28
+ res = requests.get(url, headers=headers, timeout=5)
29
+ if res.status_code == 200:
30
+ return res.json()["people"]
31
+ except Exception:
32
+ pass
33
+ return None
34
+
35
+
36
+ def group_people_by_station(people):
37
+ iss_groups = defaultdict(list)
38
+ tiangong_groups = defaultdict(list)
39
+
40
+ for p in people:
41
+ craft = p.get("spacecraft", "Unknown")
42
+ if p.get("iss"):
43
+ iss_groups[craft].append(p)
44
+ else:
45
+ tiangong_groups[craft].append(p)
46
+
47
+ return iss_groups, tiangong_groups
48
+
49
+
50
+ def build_tree_menu(people_count, iss_groups, tiangong_groups):
51
+ tree = Tree(f"[bold white]PEOPLE IN SPACE ({people_count})[/bold white]")
52
+ missions = []
53
+ num = 1
54
+
55
+ iss_node = tree.add("[bold green]ISS[/bold green]")
56
+ for craft, members in iss_groups.items():
57
+ m_node = iss_node.add(f"[bold cyan]{num}. {craft}[/bold cyan]")
58
+ for m in members:
59
+ m_node.add(f"{m.get('name')} ({m.get('country')})")
60
+ missions.append((craft, members))
61
+ num += 1
62
+
63
+ tiangong_node = tree.add("[bold deep_sky_blue]Tiangong[/bold deep_sky_blue]")
64
+ for craft, members in tiangong_groups.items():
65
+ m_node = tiangong_node.add(f"[bold cyan]{num}. {craft}[/bold cyan]")
66
+ for m in members:
67
+ m_node.add(f"{m.get('name')} ({m.get('country')})")
68
+ missions.append((craft, members))
69
+ num += 1
70
+
71
+ return tree, missions
72
+
73
+
74
+ def fetch_photo_panel(url):
75
+ if not url:
76
+ return Panel(Text("No photo", style="dim red"), title="Photo", expand=False)
77
+
78
+ try:
79
+ res = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=5)
80
+ if res.status_code == 200:
81
+ img = Image.open(BytesIO(res.content)).convert("RGB")
82
+ width, height = img.size
83
+ pixels = img.tobytes()
84
+
85
+ config = chafa.CanvasConfig()
86
+ config.width = 28
87
+ config.height = 16
88
+
89
+ canvas = chafa.Canvas(config)
90
+ canvas.draw_all_pixels(
91
+ chafa.PixelType.CHAFA_PIXEL_RGB8, pixels, width, height, width * 3
92
+ )
93
+ out = canvas.print().decode("utf-8")
94
+
95
+ return Panel(Text.from_ansi(out), title="Photo", expand=False)
96
+ except Exception:
97
+ pass
98
+
99
+ return Panel(Text("No photo", style="dim red"), title="Photo", expand=False)
100
+
101
+
102
+ def create_mission_table(craft, members):
103
+ table = Table(title=f"Mission: {craft}")
104
+ table.add_column("No.", style="cyan", justify="right")
105
+ table.add_column("Name", style="bold white")
106
+ table.add_column("Country", style="green")
107
+
108
+ for i, m in enumerate(members, 1):
109
+ table.add_row(str(i), str(m.get("name")), str(m.get("country")))
110
+
111
+ return table
112
+
113
+
114
+ def calculate_space_experience(launched_timestamp, previous_days):
115
+ if not launched_timestamp:
116
+ return "N/A", f"{previous_days} days"
117
+
118
+ dt = datetime.fromtimestamp(launched_timestamp)
119
+ launched_str = dt.strftime("%Y-%m-%d %H:%M UTC")
120
+
121
+ current_mission_days = (time.time() - launched_timestamp) / 86400
122
+ total_days = previous_days + current_mission_days
123
+
124
+ total_str = f"{total_days:.1f} days ({previous_days}p + {current_mission_days:.1f}c)"
125
+ return launched_str, total_str
126
+
127
+
128
+ def create_profile_table(person):
129
+ table = Table(title=f"Profile: {person.get('name')}")
130
+ table.add_column("Property", style="cyan")
131
+ table.add_column("Value", style="white", max_width=40)
132
+
133
+ table.add_row("Country", f"{person.get('country')} {person.get('flag', '')}")
134
+ table.add_row("Position", str(person.get("position") or "N/A"))
135
+ table.add_row("Agency", str(person.get("agency") or "N/A"))
136
+ table.add_row("Spacecraft", str(person.get("spacecraft") or "N/A"))
137
+
138
+ launched_ts = person.get("launched")
139
+ prev_days = person.get("days_in_space", 0)
140
+ launched_date, total_experience = calculate_space_experience(launched_ts, prev_days)
141
+
142
+ table.add_row("Launched At", launched_date)
143
+ table.add_row("Time in Space", total_experience)
144
+
145
+ links = [
146
+ ("Image URL", person.get("image")),
147
+ ("Wikipedia", person.get("url")),
148
+ ("X (Twitter)", person.get("twitter")),
149
+ ("Instagram", person.get("instagram")),
150
+ ]
151
+
152
+ for label, link_url in links:
153
+ if link_url:
154
+ table.add_row(label, f"[link={link_url}]Open Link[/link]")
155
+ else:
156
+ table.add_row(label, "N/A")
157
+
158
+ return table
159
+
160
+
161
+ def show_astronaut_view(person):
162
+ clear_screen()
163
+ photo_panel = fetch_photo_panel(person.get("image"))
164
+ profile_table = create_profile_table(person)
165
+ console.print(Columns([photo_panel, profile_table]))
166
+ input("\nPress Enter to return to menu...")
167
+
168
+
169
+ def handle_mission_view(craft, members):
170
+ clear_screen()
171
+ console.print(create_mission_table(craft, members))
172
+
173
+ astro_choice = input("\nSelect astronaut number (or 'menu'/'quit'): ").strip().lower()
174
+
175
+ if astro_choice == "quit":
176
+ return "quit"
177
+ elif astro_choice == "menu":
178
+ return "menu"
179
+
180
+ if astro_choice.isdigit():
181
+ a_idx = int(astro_choice) - 1
182
+ if 0 <= a_idx < len(members):
183
+ show_astronaut_view(members[a_idx])
184
+
185
+ return "ok"
186
+
187
+
188
+ def main():
189
+ while True:
190
+ clear_screen()
191
+ people = fetch_space_data()
192
+
193
+ if people is None:
194
+ console.print("[bold red]Error: No internet connection. Please check your network and try again.[/bold red]")
195
+ input("\nPress Enter to retry...")
196
+ continue
197
+
198
+ iss_groups, tiangong_groups = group_people_by_station(people)
199
+ tree, missions = build_tree_menu(len(people), iss_groups, tiangong_groups)
200
+
201
+ console.print(tree)
202
+ choice = input("\n> ").strip().lower()
203
+
204
+ if choice == "quit":
205
+ break
206
+ elif choice == "menu":
207
+ continue
208
+
209
+ if choice.isdigit():
210
+ idx = int(choice) - 1
211
+ if 0 <= idx < len(missions):
212
+ craft, members = missions[idx]
213
+ result = handle_mission_view(craft, members)
214
+ if result == "quit":
215
+ break
216
+
217
+
218
+ if __name__ == "__main__":
219
+ main()