gtime 0.3.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.
gtime-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Savitoj Singh
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.
gtime-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtime
3
+ Version: 0.3.0
4
+ Summary: Global Time Utility (gtime) - A modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode
5
+ Author-email: Savitoj Singh <savv@duck.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/savitojs/gtime
8
+ Project-URL: Repository, https://github.com/savitojs/gtime
9
+ Project-URL: Bug Tracker, https://github.com/savitojs/gtime/issues
10
+ Project-URL: Documentation, https://github.com/savitojs/gtime#readme
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rich
15
+ Requires-Dist: python-dateutil
16
+ Requires-Dist: thefuzz
17
+ Requires-Dist: pytz; python_version < "3.9"
18
+ Dynamic: license-file
19
+
20
+ # gtime 🌐
21
+
22
+ gtime (Global Time) is a modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode
23
+
24
+ ## Features
25
+ - Fast city lookup with fuzzy search and suggestions
26
+ - Add/remove/list favorite cities
27
+ - Compare times for multiple cities
28
+ - Meeting time conversion across favorites
29
+ - Live/watch mode for real-time updates
30
+ - Colorful, user-friendly output (using Rich)
31
+ - Comprehensive test suite (pytest)
32
+ - Performance-optimized for large city databases
33
+
34
+ ## Installation (from source)
35
+ Clone the repo and install locally:
36
+
37
+ ```sh
38
+ pip install .
39
+ ```
40
+
41
+ Or, install from PyPI:
42
+
43
+ ```sh
44
+ pip install gtime
45
+ ```
46
+ ## Demo
47
+
48
+ **Note:** Some command output may appear broken in the demo, but it works correctly in real use
49
+
50
+ ![demo](./assets/demo.gif)
51
+
52
+ ## Usage
53
+ After installation, run the CLI:
54
+
55
+ ```sh
56
+ gtime [command] [arguments]
57
+ ```
58
+
59
+ Or as a module:
60
+
61
+ ```sh
62
+ python -m gtime.cli [command] [arguments]
63
+ ```
64
+
65
+ Example commands:
66
+ - `gtime London` — Show time for London
67
+ - `gtime add Tokyo` — Add Tokyo to favorites
68
+ - `gtime list` — List favorite cities
69
+ - `gtime compare London Tokyo` — Compare cities
70
+ - `gtime meeting at 10:00 AM` — Meeting time conversion
71
+ - `gtime watch` — Live mode
72
+
73
+ ## Development & Publishing
74
+
75
+ ### GitHub Actions
76
+ This project includes automated workflows:
77
+ - **Tests**: Runs on every push/PR across Python 3.8-3.12
78
+ - **Publish**: Automatically publishes to PyPI upon new GitHub release
79
+
80
+ ## License
81
+ MIT
gtime-0.3.0/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # gtime 🌐
2
+
3
+ gtime (Global Time) is a modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode
4
+
5
+ ## Features
6
+ - Fast city lookup with fuzzy search and suggestions
7
+ - Add/remove/list favorite cities
8
+ - Compare times for multiple cities
9
+ - Meeting time conversion across favorites
10
+ - Live/watch mode for real-time updates
11
+ - Colorful, user-friendly output (using Rich)
12
+ - Comprehensive test suite (pytest)
13
+ - Performance-optimized for large city databases
14
+
15
+ ## Installation (from source)
16
+ Clone the repo and install locally:
17
+
18
+ ```sh
19
+ pip install .
20
+ ```
21
+
22
+ Or, install from PyPI:
23
+
24
+ ```sh
25
+ pip install gtime
26
+ ```
27
+ ## Demo
28
+
29
+ **Note:** Some command output may appear broken in the demo, but it works correctly in real use
30
+
31
+ ![demo](./assets/demo.gif)
32
+
33
+ ## Usage
34
+ After installation, run the CLI:
35
+
36
+ ```sh
37
+ gtime [command] [arguments]
38
+ ```
39
+
40
+ Or as a module:
41
+
42
+ ```sh
43
+ python -m gtime.cli [command] [arguments]
44
+ ```
45
+
46
+ Example commands:
47
+ - `gtime London` — Show time for London
48
+ - `gtime add Tokyo` — Add Tokyo to favorites
49
+ - `gtime list` — List favorite cities
50
+ - `gtime compare London Tokyo` — Compare cities
51
+ - `gtime meeting at 10:00 AM` — Meeting time conversion
52
+ - `gtime watch` — Live mode
53
+
54
+ ## Development & Publishing
55
+
56
+ ### GitHub Actions
57
+ This project includes automated workflows:
58
+ - **Tests**: Runs on every push/PR across Python 3.8-3.12
59
+ - **Publish**: Automatically publishes to PyPI upon new GitHub release
60
+
61
+ ## License
62
+ MIT
@@ -0,0 +1 @@
1
+ # Global Time Utility (gtime) package
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ CLI entry point for Global Time Utility (gtime)
6
+ This module provides the command line interface for the Global Time Utility (gtime) application,
7
+ allowing users to view current times in various cities, manage favorites, and more.
8
+ """
9
+
10
+ import sys
11
+ import os
12
+ import json
13
+ import datetime
14
+ from pathlib import Path
15
+ from typing import List, Tuple, Optional
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+ from rich.prompt import Prompt
19
+ from rich.text import Text
20
+ from rich.panel import Panel
21
+ from rich.align import Align
22
+ from rich.box import ROUNDED
23
+ import random
24
+ import time
25
+
26
+ from .core import (
27
+ load_favorites, save_favorites, get_city_by_name, fuzzy_search_city, suggest_cities,
28
+ get_time_emoji, get_greeting, get_funny_footer
29
+ )
30
+ from .data import CITY_DB
31
+
32
+ console = Console()
33
+ FAV_FILE = Path.home() / ".gtime_favorites.json"
34
+
35
+ def print_city_time(city, country, tz, emoji, meeting_time: Optional[datetime.datetime] = None):
36
+ from .core import ZoneInfo
37
+ now = datetime.datetime.now(ZoneInfo(tz))
38
+ dt = meeting_time.astimezone(ZoneInfo(tz)) if meeting_time else now
39
+ hour = dt.hour
40
+ emoji_time = get_time_emoji(hour)
41
+ greeting = get_greeting(hour)
42
+ footer = get_funny_footer(city, hour)
43
+ offset = dt.utcoffset()
44
+ if offset is not None:
45
+ total_minutes = offset.total_seconds() / 60
46
+ hours = int(total_minutes // 60)
47
+ minutes = int(abs(total_minutes) % 60)
48
+ sign = '+' if hours >= 0 else '-'
49
+ offset_str = f'UTC{sign}{abs(hours)}' + (f':{minutes:02}' if minutes else '')
50
+ else:
51
+ offset_str = 'UTC?'
52
+ table = Table(show_header=False, box=None)
53
+ table.add_row(f"[bold cyan]{emoji} {city}, {country}[/bold cyan]")
54
+ table.add_row(f"[green]{dt.strftime('%A, %B %d, %Y')}[/green]")
55
+ table.add_row(f"[yellow]{dt.strftime('%I:%M %p')} {emoji_time} ([white]{offset_str}[/white])[/yellow]")
56
+ table.add_row("")
57
+ table.add_row(f"[italic magenta]{footer}[/italic magenta]")
58
+ console.print(Panel(table, title=f"{greeting}!", expand=False))
59
+
60
+ def print_favorites(favs: List[str], meeting_time: Optional[datetime.datetime] = None):
61
+ from .core import ZoneInfo
62
+ if not favs:
63
+ console.print("[red]No favorite cities set. Use 'gtime add <city>' to add one.[/red]")
64
+ console.print("[yellow]Use 'gtime <city>' to search one and 'gtime --help' for more info[/yellow]")
65
+ return
66
+ banner = Text("🌍 GLOBAL TIME FAVORITES 🌍", style="bold magenta on cyan", justify="center")
67
+ console.print(Align.center(banner))
68
+ table = Table(title=None, show_lines=True, box=ROUNDED, expand=False)
69
+ table.add_column("Flag", style="bold", justify="center")
70
+ table.add_column("City", style="bold cyan")
71
+ table.add_column("Local Time", style="green")
72
+ table.add_column("Phase", style="magenta")
73
+ table.add_column("UTC Offset", style="yellow")
74
+ for fav in favs:
75
+ city_info = get_city_by_name(fav)
76
+ if not city_info:
77
+ continue
78
+ city, country, tz, emoji = city_info
79
+ now = datetime.datetime.now(ZoneInfo(tz))
80
+ dt = meeting_time.astimezone(ZoneInfo(tz)) if meeting_time else now
81
+ hour = dt.hour
82
+ emoji_time = get_time_emoji(hour)
83
+ phase = get_greeting(hour)
84
+ offset = dt.utcoffset()
85
+ if offset is not None:
86
+ total_minutes = offset.total_seconds() / 60
87
+ hours = int(total_minutes // 60)
88
+ minutes = int(abs(total_minutes) % 60)
89
+ sign = '+' if hours >= 0 else '-'
90
+ offset_str = f'UTC{sign}{abs(hours)}' + (f':{minutes:02}' if minutes else '')
91
+ else:
92
+ offset_str = 'UTC?'
93
+ clock_emoji = chr(0x1F550 + ((hour-1)%12)) if 1 <= hour <= 12 else '🕰️'
94
+ table.add_row(
95
+ emoji, f"{city}, {country}", f"{dt.strftime('%a, %b %d %I:%M %p')} {clock_emoji}", f"{emoji_time} {phase}", offset_str
96
+ )
97
+ fun_facts = [
98
+ "Did you know? There are 24 time zones in the world! 🌐",
99
+ "UTC stands for Universal Time Coordinated! 🕒",
100
+ "Some countries have 30 or 45 minute offsets! ⏰",
101
+ "The world is a beautiful place—enjoy every timezone! 🌏",
102
+ "Time flies like an arrow. Fruit flies like a banana! 🍌",
103
+ "It's always 5 o'clock somewhere! 🍹",
104
+ "China uses only one time zone despite spanning 5 geographical zones! 🇨🇳",
105
+ "Russia has 11 time zones - the most of any country! 🇷🇺",
106
+ "The International Date Line isn't straight - it zigzags! 📅",
107
+ "Some Pacific islands are a full day ahead of others! 🏝️",
108
+ "Nepal has a unique +5:45 UTC offset - not a round hour! 🏔️",
109
+ "Australia's Lord Howe Island has a 30-minute daylight saving! ⏰",
110
+ "The North and South Poles technically have all time zones! 🧭",
111
+ "France has the most time zones (12) due to overseas territories! 🇫🇷",
112
+ "Arizona (mostly) doesn't observe daylight saving time! 🌵",
113
+ "Time zones were invented by railway companies! 🚂",
114
+ "Before time zones, every city had its own local time! 🏙️",
115
+ "The first country to see the new year is Kiribati! 🎉",
116
+ "GMT and UTC are almost the same but not exactly! ⏱️",
117
+ "Some countries have changed time zones for political reasons! 🗳️",
118
+ ]
119
+ footer = random.choice(fun_facts)
120
+ panel = Panel(table, title="[bold magenta]Your Favorite Cities[/bold magenta]", subtitle=f"[italic cyan]{footer}", border_style="bright_magenta", box=ROUNDED)
121
+ console.print(panel)
122
+
123
+ def print_compare(cities: List[str]):
124
+ from .core import ZoneInfo
125
+ found = []
126
+ for name in cities:
127
+ city_info = get_city_by_name(name)
128
+ if city_info:
129
+ found.append(city_info)
130
+ else:
131
+ console.print(f"[red]City not found:[/red] {name}")
132
+ if not found:
133
+ console.print("[red]No valid cities to compare.[/red]")
134
+ return
135
+ table = Table(title="[bold magenta]Global Time Compare[/bold magenta]", show_lines=True, box=ROUNDED, expand=False)
136
+ table.add_column("Flag", style="bold", justify="center")
137
+ table.add_column("City", style="bold cyan")
138
+ table.add_column("Local Time", style="green")
139
+ table.add_column("Phase", style="magenta")
140
+ table.add_column("UTC Offset", style="yellow")
141
+ for city, country, tz, emoji in found:
142
+ now = datetime.datetime.now(ZoneInfo(tz))
143
+ hour = now.hour
144
+ emoji_time = get_time_emoji(hour)
145
+ phase = get_greeting(hour)
146
+ offset = now.utcoffset()
147
+ if offset is not None:
148
+ total_minutes = offset.total_seconds() / 60
149
+ hours = int(total_minutes // 60)
150
+ minutes = int(abs(total_minutes) % 60)
151
+ sign = '+' if hours >= 0 else '-'
152
+ offset_str = f'UTC{sign}{abs(hours)}' + (f':{minutes:02}' if minutes else '')
153
+ else:
154
+ offset_str = 'UTC?'
155
+ clock_emoji = chr(0x1F550 + ((hour-1)%12)) if 1 <= hour <= 12 else '🕰️'
156
+ table.add_row(
157
+ emoji, f"{city}, {country}", f"{now.strftime('%a, %b %d %I:%M %p')} {clock_emoji}", f"{emoji_time} {phase}", offset_str
158
+ )
159
+ console.print(table)
160
+
161
+ def watch_mode(func, *args, **kwargs):
162
+ try:
163
+ while True:
164
+ os.system('clear')
165
+ func(*args, **kwargs)
166
+
167
+ # Countdown timer for next refresh
168
+ for seconds_left in range(60, 0, -1):
169
+ console.print(f"[dim]Press Ctrl+C to exit watch mode. Next refresh in {seconds_left} seconds...[/dim]", end="\r")
170
+ time.sleep(1)
171
+
172
+ except KeyboardInterrupt:
173
+ console.print("\n[green]Exited watch mode.[/green]")
174
+
175
+ def parse_meeting_time(args: List[str]) -> Optional[datetime.datetime]:
176
+ if "at" in args:
177
+ idx = args.index("at")
178
+ elif "on" in args:
179
+ idx = args.index("on")
180
+ else:
181
+ return None
182
+ time_str = " ".join(args[idx+1:])
183
+ try:
184
+ today = datetime.datetime.now()
185
+ dt = datetime.datetime.strptime(time_str, "%I:%M %p")
186
+ meeting_time = today.replace(hour=dt.hour, minute=dt.minute, second=0, microsecond=0)
187
+ return meeting_time
188
+ except Exception:
189
+ return None
190
+
191
+ def print_help():
192
+ help_text = """
193
+ [bold cyan]gtime - Global Time Utility[/bold cyan]
194
+
195
+ [bold yellow]Usage:[/bold yellow]
196
+ gtime [command] [arguments]
197
+ gtime <city name>
198
+
199
+ [bold yellow]Commands:[/bold yellow]
200
+ [green]add <city>[/green] Add a city to your favorites
201
+ [green]remove <city>[/green] Remove a city from your favorites
202
+ [green]list[/green] List your favorite cities and their current times
203
+ [green]list --watch[/green] Watch mode: continuously refresh your favorites list every 60 seconds
204
+ [green]meeting at / on <time>[/green] Show favorite cities' times for a meeting (e.g. 'meeting at 10:00 AM' or 'meeting on 10:00 AM')
205
+ [green]compare <city1> <city2> ...[/green] Compare times for multiple cities
206
+ [green]compare <city1> <city2> ... --watch[/green] Watch mode: continuously refresh city comparison
207
+ [green]watch[/green] Same as 'list --watch' - watch your favorites in real-time
208
+ [green]<city name>[/green] Show the current time for any city (fuzzy search supported)
209
+ [green]-h, --help[/green] Show this help message
210
+
211
+ [bold yellow]Watch Mode:[/bold yellow]
212
+ Use [green]--watch[/green] with list or compare commands, or use [green]watch[/green] alone to continuously
213
+ refresh the display every 60 seconds with a live countdown timer. Press Ctrl+C to exit.
214
+ """
215
+ console.print(help_text)
216
+
217
+ def main():
218
+ args = sys.argv[1:]
219
+ if args and args[0] in ('-h', '--help'):
220
+ print_help()
221
+ return
222
+ favs = load_favorites()
223
+ local_hour = datetime.datetime.now().hour
224
+ greeting = get_greeting(local_hour)
225
+ try:
226
+ user = os.getlogin()
227
+ except Exception:
228
+ user = "user"
229
+ console.print(f"[bold blue]{greeting}, {user}! Welcome to Global Time Utility 🌐[/bold blue]")
230
+
231
+ if not args:
232
+ print_favorites(favs)
233
+ return
234
+
235
+ cmd = args[0].lower()
236
+
237
+ # Watch mode for list and compare
238
+ if cmd == "watch" or (cmd == "list" and len(args) > 1 and args[1] == "--watch"):
239
+ watch_mode(print_favorites, favs)
240
+ return
241
+ if cmd == "compare" and (len(args) > 2 and args[-1] == "--watch"):
242
+ watch_mode(print_compare, [c for c in args[1:-1]])
243
+ return
244
+
245
+ if cmd == "add" and len(args) > 1:
246
+ city_info = get_city_by_name(" ".join(args[1:]))
247
+ if city_info:
248
+ city, *_ = city_info
249
+ if city not in favs:
250
+ favs.append(city)
251
+ save_favorites(favs)
252
+ console.print(f"[green]Added {city} to favorites![/green]")
253
+ else:
254
+ console.print(f"[yellow]{city} is already in favorites.[/yellow]")
255
+ else:
256
+ console.print("[red]City not found.[/red]")
257
+ suggestions = suggest_cities(" ".join(args[1:]))
258
+ if suggestions:
259
+ console.print(f"[yellow]Did you mean:[/yellow] {', '.join(suggestions)}")
260
+ return
261
+
262
+ if cmd == "remove" and len(args) > 1:
263
+ city = " ".join(args[1:])
264
+ if city in favs:
265
+ favs.remove(city)
266
+ save_favorites(favs)
267
+ console.print(f"[green]Removed {city} from favorites.[/green]")
268
+ else:
269
+ console.print(f"[yellow]{city} is not in favorites.[/yellow]")
270
+ return
271
+
272
+ if cmd == "list":
273
+ print_favorites(favs)
274
+ return
275
+
276
+ if cmd == "meeting":
277
+ if len(args) == 1:
278
+ print_favorites(favs)
279
+ return
280
+ meeting_time = parse_meeting_time(args)
281
+ if meeting_time is None:
282
+ console.print("[red]Invalid meeting command. Use: 'meeting at/on <time>' (e.g. 'meeting at 10:00 AM' or 'meeting on 10:00 AM').[/red]")
283
+ console.print("[yellow]See 'gtime -h' for help.[/yellow]")
284
+ return
285
+ print_favorites(favs, meeting_time)
286
+ return
287
+
288
+ if cmd == "compare" and len(args) > 1:
289
+ not_found = []
290
+ found = []
291
+ for name in args[1:]:
292
+ if name == "--watch":
293
+ continue
294
+ city_info = get_city_by_name(name)
295
+ if city_info:
296
+ found.append(city_info)
297
+ else:
298
+ not_found.append(name)
299
+ if not_found:
300
+ for nf in not_found:
301
+ console.print(f"[red]City not found:[/red] {nf}")
302
+ suggestions = suggest_cities(nf)
303
+ if suggestions:
304
+ console.print(f"[yellow]Did you mean:[/yellow] {', '.join(suggestions)}")
305
+ if found:
306
+ print_compare([c[0] for c in found])
307
+ else:
308
+ console.print("[red]No valid cities to compare.[/red]")
309
+ return
310
+
311
+ # If command is not recognized, try city lookup, else error
312
+ city_info = get_city_by_name(" ".join(args))
313
+ if city_info:
314
+ print_city_time(*city_info)
315
+ else:
316
+ console.print("[red]Invalid command or city not found. See 'gtime -h' for help.[/red]")
317
+ suggestions = suggest_cities(" ".join(args))
318
+ if suggestions:
319
+ console.print(f"[yellow]Did you mean:[/yellow] {', '.join(suggestions)}")
320
+
321
+ if __name__ == "__main__":
322
+ main()
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Core logic for Global Time Utility (gtime) lookup, fuzzy search, helpers
6
+ """
7
+
8
+ import datetime
9
+ import json
10
+ from pathlib import Path
11
+ from typing import List, Tuple, Optional
12
+ import random
13
+ from functools import lru_cache
14
+
15
+ try:
16
+ from zoneinfo import ZoneInfo
17
+ except ImportError:
18
+ from pytz import timezone as ZoneInfo
19
+
20
+ from .data import CITY_DB
21
+
22
+ FAV_FILE = Path.home() / ".gtime_favorites.json"
23
+
24
+ def load_favorites() -> List[str]:
25
+ if FAV_FILE.exists():
26
+ try:
27
+ with open(FAV_FILE, "r") as f:
28
+ return json.load(f)
29
+ except Exception:
30
+ return []
31
+ return []
32
+
33
+ def save_favorites(favs: List[str]) -> None:
34
+ with open(FAV_FILE, "w") as f:
35
+ json.dump(favs, f)
36
+
37
+ _city_names_cache = None
38
+ _city_name_to_index = None
39
+
40
+ def _get_city_names():
41
+ global _city_names_cache, _city_name_to_index
42
+ if _city_names_cache is None or len(_city_names_cache) != len(CITY_DB):
43
+ _city_names_cache = [f"{city} ({country})" for city, country, _, _ in CITY_DB]
44
+ _city_name_to_index = {name: idx for idx, name in enumerate(_city_names_cache)}
45
+ return _city_names_cache, _city_name_to_index
46
+
47
+ @lru_cache(maxsize=256)
48
+ def fuzzy_search_city(query: str) -> Optional[Tuple[str, str, str, str]]:
49
+ from thefuzz import process
50
+ names, name_to_idx = _get_city_names()
51
+ match, score = process.extractOne(query, names)
52
+ if score > 60:
53
+ idx = name_to_idx[match]
54
+ return CITY_DB[idx]
55
+ return None
56
+
57
+ @lru_cache(maxsize=256)
58
+ def get_city_by_name(city_name: str) -> Optional[Tuple[str, str, str, str]]:
59
+ for city, country, tz, emoji in CITY_DB:
60
+ if city.lower() == city_name.lower():
61
+ return (city, country, tz, emoji)
62
+ from thefuzz import process
63
+ names, name_to_idx = _get_city_names()
64
+ matches = process.extract(city_name, names, limit=3)
65
+ if matches and matches[0][1] > 60:
66
+ idx = name_to_idx[matches[0][0]]
67
+ return CITY_DB[idx]
68
+ return None
69
+
70
+ def suggest_cities(city_name: str) -> List[str]:
71
+ from thefuzz import process
72
+ names, _ = _get_city_names()
73
+ matches = process.extract(city_name, names, limit=3)
74
+ suggestions = [m[0] for m in matches if m[1] > 40]
75
+ return suggestions
76
+
77
+ def get_time_emoji(hour: int) -> str:
78
+ if 5 <= hour < 12:
79
+ return "🌅"
80
+ elif 12 <= hour < 17:
81
+ return "☀️"
82
+ elif 17 <= hour < 21:
83
+ return "🌆"
84
+ else:
85
+ return "🌙"
86
+
87
+ def get_greeting(hour: int) -> str:
88
+ if 5 <= hour < 12:
89
+ return "Good morning"
90
+ elif 12 <= hour < 17:
91
+ return "Good afternoon"
92
+ elif 17 <= hour < 21:
93
+ return "Good evening"
94
+ else:
95
+ return "Good night"
96
+
97
+ def get_funny_footer(city: str, hour: int) -> str:
98
+ night_jokes = [
99
+ f"It's late in {city}. Don't let the bed bugs bite! 🛌",
100
+ f"{city} is sleeping. Or are you a night owl? 🦉",
101
+ f"Shhh... {city} is dreaming. 😴",
102
+ f"The stars are shining bright over {city}! ✨",
103
+ f"Midnight snack time in {city}? 🍕",
104
+ f"The city that never sleeps? Not {city} right now! 💤",
105
+ f"Late night thoughts from {city}... 💭",
106
+ f"Even {city} needs beauty sleep! 💄",
107
+ f"Night shift workers in {city} are keeping busy! 🌃",
108
+ f"Sweet dreams from {city}! 🌙",
109
+ f"Time for some beauty sleep in {city}! 💤",
110
+ f"The moon is watching over {city} tonight! 🌛",
111
+ f"Counting sheep in {city}... 1, 2, 3... 🐑",
112
+ f"Pizza delivery is probably still open in {city}! 🍕",
113
+ f"Time to binge-watch something in {city}! 📺",
114
+ f"Late night coding session in {city}? 💻",
115
+ f"The night is young in {city}! 🌃",
116
+ f"Peaceful slumber awaits in {city}! 😴",
117
+ f"Night photography weather in {city}! 📸",
118
+ f"The city is tucked in for the night in {city}! 🛏️",
119
+ f"Insomniacs unite in {city}! 😵",
120
+ f"Time for a bedtime story in {city}! 📚",
121
+ f"The witching hour in {city}! 🧙‍♀️",
122
+ f"Dreaming of better days in {city}! 💭",
123
+ f"Silent streets in {city} tell stories! 🏙️",
124
+ ]
125
+ morning_jokes = [
126
+ f"Rise and shine, {city}! ☀️",
127
+ f"Coffee time in {city}? ☕",
128
+ f"Start your engines, {city}! 🚗",
129
+ f"The early bird catches the worm in {city}! 🐦",
130
+ f"Fresh morning air in {city}! 🌬️",
131
+ f"Time to seize the day in {city}! 💪",
132
+ f"Morning jog weather in {city}? 🏃",
133
+ f"Breakfast is the most important meal in {city}! 🥞",
134
+ f"The sun is greeting {city} with a smile! 😊",
135
+ f"New day, new possibilities in {city}! 🌈",
136
+ f"Rush hour is starting in {city}! 🚌",
137
+ f"Morning news is on in {city}! 📺",
138
+ f"Good morning sunshine from {city}! 🌞",
139
+ f"Fresh croissants and coffee in {city}? 🥐",
140
+ f"Morning yoga session in {city}? 🧘‍♀️",
141
+ f"Alarm clocks are ringing in {city}! ⏰",
142
+ f"Another beautiful morning in {city}! 🌸",
143
+ f"Time to make your bed in {city}! 🛏️",
144
+ f"Fresh start vibes in {city}! ✨",
145
+ f"Morning commute begins in {city}! 🚇",
146
+ f"Time to water the plants in {city}! 🪴",
147
+ f"Birds are chirping in {city}! 🐦",
148
+ f"Morning motivation mode in {city}! 💪",
149
+ f"The world is your oyster in {city}! 🦪",
150
+ f"Sunrise spectacular in {city}! 🌅",
151
+ f"Fresh as a daisy in {city}! 🌼",
152
+ f"Morning mindfulness in {city}! 🧠",
153
+ f"Early bird specials in {city}! 🍳",
154
+ ]
155
+ afternoon_jokes = [
156
+ f"Keep hustling, {city}! 💪",
157
+ f"Perfect time for a siesta in {city}. 😴",
158
+ f"Hope your day is going well in {city}! 🌞",
159
+ f"Lunch break time in {city}? 🍽️",
160
+ f"The sun is at its peak in {city}! ☀️",
161
+ f"Productivity mode activated in {city}! 📈",
162
+ f"Ice cream weather in {city}? 🍦",
163
+ f"Working hard or hardly working in {city}? 💼",
164
+ f"The afternoon hustle in {city} is real! 🏃‍♀️",
165
+ f"Time flies when you're having fun in {city}! ⏰",
166
+ f"Midday energy boost needed in {city}? ⚡",
167
+ f"The perfect time for outdoor activities in {city}! 🌳",
168
+ f"Sunshine and productivity in {city}! 🌻",
169
+ f"Time to stretch those legs in {city}! 🤸‍♂️",
170
+ f"Afternoon meeting marathon in {city}! 📊",
171
+ f"Time for a quick power walk in {city}! 🚶‍♀️",
172
+ f"Perfect weather for outdoor dining in {city}! 🍴",
173
+ f"Getting things done in {city}! ✅",
174
+ f"Halfway through the workday in {city}! 📈",
175
+ f"Afternoon delight in {city}! 🎵",
176
+ f"Keep calm and carry on in {city}! 🧘",
177
+ f"The grind never stops in {city}! ⚙️",
178
+ f"Peak performance hours in {city}! 🏆",
179
+ f"Time for a coffee break in {city}! ☕",
180
+ f"Afternoon adventures await in {city}! 🗺️",
181
+ f"Sunshine therapy in {city}! ☀️",
182
+ f"Power through the afternoon in {city}! 💪",
183
+ f"The day is in full swing in {city}! 🎯",
184
+ ]
185
+ evening_jokes = [
186
+ f"Time to relax in {city}. 🍷",
187
+ f"Sunset vibes in {city}. 🌇",
188
+ f"Netflix and chill in {city}? 🍿",
189
+ f"Happy hour somewhere in {city}! 🍻",
190
+ f"Dinner plans in {city}? 🍽️",
191
+ f"The golden hour in {city} looks magical! ✨",
192
+ f"Time to unwind in {city}! 🧘",
193
+ f"Evening stroll weather in {city}? 🚶",
194
+ f"The city lights are starting to twinkle in {city}! 💡",
195
+ f"Date night in {city}? 💕",
196
+ f"Rush hour traffic clearing up in {city}! 🚗",
197
+ f"The workday is winding down in {city}! 📝",
198
+ f"Time for some evening entertainment in {city}! 🎭",
199
+ f"Time to cook dinner in {city}! 👨‍🍳",
200
+ f"Golden hour photography in {city}! 📷",
201
+ f"Winding down in {city}... 🛋️",
202
+ f"Time for some evening exercise in {city}! 🏋️‍♀️",
203
+ f"The day is coming to an end in {city}! 🌆",
204
+ f"Perfect time for a walk in {city}! 🚶",
205
+ f"Time to catch up with friends in {city}! 👥",
206
+ f"Evening breeze in {city} feels nice! 🌬️",
207
+ f"Cozy evening vibes in {city}! 🕯️",
208
+ f"Time to unwind with a good book in {city}! 📚",
209
+ f"Twilight magic in {city}! ✨",
210
+ f"Time to reflect on the day in {city}! 💭",
211
+ f"Perfect time for a romantic dinner in {city}! 🥂",
212
+ f"Evening meditation time in {city}! 🧘‍♂️",
213
+ f"Time to call it a day in {city}! 📞",
214
+ f"The evening glow in {city} is stunning! 🌅",
215
+ f"Time for some self-care in {city}! 💆‍♀️",
216
+ ]
217
+ if 5 <= hour < 12:
218
+ return morning_jokes[hour % len(morning_jokes)]
219
+ elif 12 <= hour < 17:
220
+ return afternoon_jokes[hour % len(afternoon_jokes)]
221
+ elif 17 <= hour < 21:
222
+ return evening_jokes[hour % len(evening_jokes)]
223
+ else:
224
+ return night_jokes[hour % len(night_jokes)]
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ City database for Global Time Utility (gtime)
6
+ """
7
+
8
+ CITY_DB = [
9
+ # North America
10
+ ("New York", "USA", "America/New_York", "🗽"),
11
+ ("Los Angeles", "USA", "America/Los_Angeles", "🌴"),
12
+ ("Chicago", "USA", "America/Chicago", "🌃"),
13
+ ("San Francisco", "USA", "America/Los_Angeles", "🌉"),
14
+ ("Seattle", "USA", "America/Los_Angeles", "🌲"),
15
+ ("Boston", "USA", "America/New_York", "🎓"),
16
+ ("Washington D.C.", "USA", "America/New_York", "🏛️"),
17
+ ("Miami", "USA", "America/New_York", "🌴"),
18
+ ("Salt Lake City", "USA", "America/Denver", "🏔️"),
19
+ ("Austin", "USA", "America/Chicago", "🎸"),
20
+ ("Dallas", "USA", "America/Chicago", "🤠"),
21
+ ("Phoenix", "USA", "America/Phoenix", "🌵"),
22
+ ("Denver", "USA", "America/Denver", "⛰️"),
23
+ ("Las Vegas", "USA", "America/Los_Angeles", "🎰"),
24
+ ("Toronto", "Canada", "America/Toronto", "🦫"),
25
+ ("Vancouver", "Canada", "America/Vancouver", "🌲"),
26
+ ("Montreal", "Canada", "America/Toronto", "🥯"),
27
+ ("Calgary", "Canada", "America/Edmonton", "🐎"),
28
+ ("Ottawa", "Canada", "America/Toronto", "🍁"),
29
+ ("Mexico City", "Mexico", "America/Mexico_City", "🌮"),
30
+ ("Guadalajara", "Mexico", "America/Mexico_City", "🎶"),
31
+ ("Monterrey", "Mexico", "America/Monterrey", "🏞️"),
32
+ ("Cancun", "Mexico", "America/Cancun", "🏖️"),
33
+ # South America
34
+ ("São Paulo", "Brazil", "America/Sao_Paulo", "🌆"),
35
+ ("Rio de Janeiro", "Brazil", "America/Sao_Paulo", "🗺️"),
36
+ ("Brasília", "Brazil", "America/Sao_Paulo", "🏛️"),
37
+ ("Buenos Aires", "Argentina", "America/Argentina/Buenos_Aires", "💃"),
38
+ ("Bogotá", "Colombia", "America/Bogota", "⛰️"),
39
+ ("Lima", "Peru", "America/Lima", "🦙"),
40
+ ("Santiago", "Chile", "America/Santiago", "⛰️"),
41
+ ("Montevideo", "Uruguay", "America/Montevideo", "🏖️"),
42
+ ("Caracas", "Venezuela", "America/Caracas", "🦜"),
43
+ ("Quito", "Ecuador", "America/Guayaquil", "🌋"),
44
+ ("Asunción", "Paraguay", "America/Asuncion", "🌳"),
45
+ ("La Paz", "Bolivia", "America/La_Paz", "🏔️"),
46
+ # Europe
47
+ ("London", "UK", "Europe/London", "🎡"),
48
+ ("Manchester", "UK", "Europe/London", "⚽"),
49
+ ("Edinburgh", "UK", "Europe/London", "🏰"),
50
+ ("Belfast", "UK", "Europe/London", "🧃"),
51
+ ("Berlin", "Germany", "Europe/Berlin", "🕍"),
52
+ ("Munich", "Germany", "Europe/Berlin", "🍺"),
53
+ ("Frankfurt", "Germany", "Europe/Berlin", "🏦"),
54
+ ("Hamburg", "Germany", "Europe/Berlin", "⚓"),
55
+ ("Paris", "France", "Europe/Paris", "🗼"),
56
+ ("Lyon", "France", "Europe/Paris", "🍷"),
57
+ ("Marseille", "France", "Europe/Paris", "⚓"),
58
+ ("Amsterdam", "Netherlands", "Europe/Amsterdam", "🚲"),
59
+ ("Rotterdam", "Netherlands", "Europe/Amsterdam", "🚢"),
60
+ ("Zurich", "Switzerland", "Europe/Zurich", "🏦"),
61
+ ("Geneva", "Switzerland", "Europe/Zurich", "🕊️"),
62
+ ("Stockholm", "Sweden", "Europe/Stockholm", "🚤"),
63
+ ("Gothenburg", "Sweden", "Europe/Stockholm", "⚓"),
64
+ ("Helsinki", "Finland", "Europe/Helsinki", "🦌"),
65
+ ("Dublin", "Ireland", "Europe/Dublin", "🍀"),
66
+ ("Rome", "Italy", "Europe/Rome", "🏛️"),
67
+ ("Milan", "Italy", "Europe/Rome", "👗"),
68
+ ("Madrid", "Spain", "Europe/Madrid", "💃"),
69
+ ("Barcelona", "Spain", "Europe/Madrid", "🏖️"),
70
+ ("Lisbon", "Portugal", "Europe/Lisbon", "🌉"),
71
+ ("Porto", "Portugal", "Europe/Lisbon", "🍷"),
72
+ ("Vienna", "Austria", "Europe/Vienna", "🎶"),
73
+ ("Warsaw", "Poland", "Europe/Warsaw", "🎩"),
74
+ ("Krakow", "Poland", "Europe/Warsaw", "🐉"),
75
+ ("Prague", "Czech Republic", "Europe/Prague", "🏰"),
76
+ ("Brno", "Czech Republic", "Europe/Prague", "🎓"),
77
+ ("Budapest", "Hungary", "Europe/Budapest", "🌉"),
78
+ ("Bucharest", "Romania", "Europe/Bucharest", "🏛️"),
79
+ ("Moscow", "Russia", "Europe/Moscow", "🎠"),
80
+ ("Saint Petersburg", "Russia", "Europe/Moscow", "🎨"),
81
+ ("Oslo", "Norway", "Europe/Oslo", "🛳️"),
82
+ ("Copenhagen", "Denmark", "Europe/Copenhagen", "🦢"),
83
+ ("Reykjavik", "Iceland", "Atlantic/Reykjavik", "🧊"),
84
+ ("Brussels", "Belgium", "Europe/Brussels", "🍫"),
85
+ ("Luxembourg", "Luxembourg", "Europe/Luxembourg", "🏰"),
86
+ ("Athens", "Greece", "Europe/Athens", "🏛️"),
87
+ ("Sofia", "Bulgaria", "Europe/Sofia", "🦁"),
88
+ ("Zagreb", "Croatia", "Europe/Zagreb", "🏟️"),
89
+ ("Belgrade", "Serbia", "Europe/Belgrade", "🏰"),
90
+ ("Kyiv", "Ukraine", "Europe/Kiev", "🌻"),
91
+ ("Tallinn", "Estonia", "Europe/Tallinn", "🦅"),
92
+ ("Riga", "Latvia", "Europe/Riga", "🌲"),
93
+ ("Vilnius", "Lithuania", "Europe/Vilnius", "🏰"),
94
+ ("Minsk", "Belarus", "Europe/Minsk", "🌲"),
95
+ ("Chișinău", "Moldova", "Europe/Chisinau", "🍇"),
96
+ # Asia
97
+ ("Tokyo", "Japan", "Asia/Tokyo", "🗼"),
98
+ ("Osaka", "Japan", "Asia/Tokyo", "🍜"),
99
+ ("Kyoto", "Japan", "Asia/Tokyo", "⛩️"),
100
+ ("Beijing", "China", "Asia/Shanghai", "🐉"),
101
+ ("Shanghai", "China", "Asia/Shanghai", "🌁"),
102
+ ("Guangzhou", "China", "Asia/Shanghai", "🌆"),
103
+ ("Shenzhen", "China", "Asia/Shanghai", "🏙️"),
104
+ ("Hong Kong", "China", "Asia/Hong_Kong", "🌃"),
105
+ ("Seoul", "South Korea", "Asia/Seoul", "🦀"),
106
+ ("Busan", "South Korea", "Asia/Seoul", "🏖️"),
107
+ ("Mumbai", "India", "Asia/Kolkata", "🌇"),
108
+ ("Delhi", "India", "Asia/Kolkata", "🕌"),
109
+ ("Bangalore", "India", "Asia/Kolkata", "🌳"),
110
+ ("Hyderabad", "India", "Asia/Kolkata", "🍛"),
111
+ ("Chennai", "India", "Asia/Kolkata", "🌊"),
112
+ ("Pune", "India", "Asia/Kolkata", "🏞️"),
113
+ ("Kolkata", "India", "Asia/Kolkata", "🌉"),
114
+ ("Singapore", "Singapore", "Asia/Singapore", "🦁"),
115
+ ("Kuala Lumpur", "Malaysia", "Asia/Kuala_Lumpur", "🌇"),
116
+ ("Bangkok", "Thailand", "Asia/Bangkok", "🛺"),
117
+ ("Manila", "Philippines", "Asia/Manila", "🏙️"),
118
+ ("Jakarta", "Indonesia", "Asia/Jakarta", "🌋"),
119
+ ("Ho Chi Minh City", "Vietnam", "Asia/Ho_Chi_Minh", "🏍️"),
120
+ ("Hanoi", "Vietnam", "Asia/Ho_Chi_Minh", "🌸"),
121
+ ("Phnom Penh", "Cambodia", "Asia/Phnom_Penh", "🛕"),
122
+ ("Vientiane", "Laos", "Asia/Vientiane", "🛕"),
123
+ ("Yangon", "Myanmar", "Asia/Yangon", "🛕"),
124
+ ("Colombo", "Sri Lanka", "Asia/Colombo", "🐘"),
125
+ ("Dhaka", "Bangladesh", "Asia/Dhaka", "🌾"),
126
+ ("Karachi", "Pakistan", "Asia/Karachi", "🌴"),
127
+ ("Lahore", "Pakistan", "Asia/Karachi", "🏰"),
128
+ ("Islamabad", "Pakistan", "Asia/Karachi", "🕌"),
129
+ ("Kabul", "Afghanistan", "Asia/Kabul", "🕌"),
130
+ ("Tehran", "Iran", "Asia/Tehran", "🕌"),
131
+ ("Baghdad", "Iraq", "Asia/Baghdad", "🏛️"),
132
+ ("Damascus", "Syria", "Asia/Damascus", "🏛️"),
133
+ ("Amman", "Jordan", "Asia/Amman", "🏜️"),
134
+ ("Tel Aviv", "Israel", "Asia/Jerusalem", "🏖️"),
135
+ ("Jerusalem", "Israel", "Asia/Jerusalem", "🕍"),
136
+ ("Beirut", "Lebanon", "Asia/Beirut", "🌊"),
137
+ ("Dubai", "UAE", "Asia/Dubai", "🏙️"),
138
+ ("Abu Dhabi", "UAE", "Asia/Dubai", "🏝️"),
139
+ ("Riyadh", "Saudi Arabia", "Asia/Riyadh", "🏜️"),
140
+ ("Jeddah", "Saudi Arabia", "Asia/Riyadh", "🌊"),
141
+ ("Kuwait City", "Kuwait", "Asia/Kuwait", "🏜️"),
142
+ ("Doha", "Qatar", "Asia/Qatar", "🏙️"),
143
+ ("Manama", "Bahrain", "Asia/Bahrain", "🏝️"),
144
+ ("Muscat", "Oman", "Asia/Muscat", "🏜️"),
145
+ ("Sana'a", "Yemen", "Asia/Aden", "🏜️"),
146
+ ("Tashkent", "Uzbekistan", "Asia/Tashkent", "🌳"),
147
+ ("Almaty", "Kazakhstan", "Asia/Almaty", "🏔️"),
148
+ ("Bishkek", "Kyrgyzstan", "Asia/Bishkek", "🏔️"),
149
+ ("Dushanbe", "Tajikistan", "Asia/Dushanbe", "🏔️"),
150
+ ("Ashgabat", "Turkmenistan", "Asia/Ashgabat", "🏜️"),
151
+ ("Baku", "Azerbaijan", "Asia/Baku", "🌊"),
152
+ ("Yerevan", "Armenia", "Asia/Yerevan", "⛰️"),
153
+ ("Tbilisi", "Georgia", "Asia/Tbilisi", "🏞️"),
154
+ ("Kathmandu", "Nepal", "Asia/Kathmandu", "🏔️"),
155
+ ("Thimphu", "Bhutan", "Asia/Thimphu", "⛰️"),
156
+ ("Ulaanbaatar", "Mongolia", "Asia/Ulaanbaatar", "🐎"),
157
+ ("Pyongyang", "North Korea", "Asia/Pyongyang", "🏛️"),
158
+ ("Taipei", "Taiwan", "Asia/Taipei", "🦋"),
159
+ ("Macau", "China", "Asia/Macau", "🎰"),
160
+ # Africa
161
+ ("Johannesburg", "South Africa", "Africa/Johannesburg", "🦁"),
162
+ ("Cape Town", "South Africa", "Africa/Johannesburg", "⛰️"),
163
+ ("Cairo", "Egypt", "Africa/Cairo", "🕌"),
164
+ ("Lagos", "Nigeria", "Africa/Lagos", "🎶"),
165
+ ("Abuja", "Nigeria", "Africa/Lagos", "🏛️"),
166
+ ("Nairobi", "Kenya", "Africa/Nairobi", "🦒"),
167
+ ("Addis Ababa", "Ethiopia", "Africa/Addis_Ababa", "☕"),
168
+ ("Casablanca", "Morocco", "Africa/Casablanca", "🌊"),
169
+ ("Rabat", "Morocco", "Africa/Casablanca", "🏰"),
170
+ ("Algiers", "Algeria", "Africa/Algiers", "🏛️"),
171
+ ("Tunis", "Tunisia", "Africa/Tunis", "🏖️"),
172
+ ("Tripoli", "Libya", "Africa/Tripoli", "🏜️"),
173
+ ("Khartoum", "Sudan", "Africa/Khartoum", "🌊"),
174
+ ("Accra", "Ghana", "Africa/Accra", "🎶"),
175
+ ("Dakar", "Senegal", "Africa/Dakar", "🦁"),
176
+ ("Abidjan", "Côte d'Ivoire", "Africa/Abidjan", "🌴"),
177
+ ("Douala", "Cameroon", "Africa/Douala", "🌴"),
178
+ ("Luanda", "Angola", "Africa/Luanda", "🦁"),
179
+ ("Harare", "Zimbabwe", "Africa/Harare", "🦓"),
180
+ ("Lusaka", "Zambia", "Africa/Lusaka", "🦏"),
181
+ ("Kampala", "Uganda", "Africa/Kampala", "🦍"),
182
+ ("Dar es Salaam", "Tanzania", "Africa/Dar_es_Salaam", "🏖️"),
183
+ ("Maputo", "Mozambique", "Africa/Maputo", "🦐"),
184
+ ("Gaborone", "Botswana", "Africa/Gaborone", "🦁"),
185
+ ("Windhoek", "Namibia", "Africa/Windhoek", "🦒"),
186
+ ("Antananarivo", "Madagascar", "Indian/Antananarivo", "🦎"),
187
+ ("Port Louis", "Mauritius", "Indian/Mauritius", "🏝️"),
188
+ # Oceania
189
+ ("Sydney", "Australia", "Australia/Sydney", "🦘"),
190
+ ("Melbourne", "Australia", "Australia/Melbourne", "🎡"),
191
+ ("Brisbane", "Australia", "Australia/Brisbane", "🌴"),
192
+ ("Perth", "Australia", "Australia/Perth", "🌅"),
193
+ ("Adelaide", "Australia", "Australia/Adelaide", "🍷"),
194
+ ("Canberra", "Australia", "Australia/Canberra", "🏛️"),
195
+ ("Darwin", "Australia", "Australia/Darwin", "🐊"),
196
+ ("Auckland", "New Zealand", "Pacific/Auckland", "🦤"),
197
+ ("Wellington", "New Zealand", "Pacific/Auckland", "🌬️"),
198
+ ("Christchurch", "New Zealand", "Pacific/Auckland", "🌸"),
199
+ ("Suva", "Fiji", "Pacific/Fiji", "🌺"),
200
+ ("Port Moresby", "Papua New Guinea", "Pacific/Port_Moresby", "🦜"),
201
+ ("Nouméa", "New Caledonia", "Pacific/Noumea", "🐚"),
202
+ ("Port Vila", "Vanuatu", "Pacific/Efate", "🌴"),
203
+ ("Honiara", "Solomon Islands", "Pacific/Guadalcanal", "🐟"),
204
+ ("Apia", "Samoa", "Pacific/Apia", "🏝️"),
205
+ ("Nuku'alofa", "Tonga", "Pacific/Tongatapu", "🌴"),
206
+ ("Ngerulmud", "Palau", "Pacific/Palau", "🐠"),
207
+ ("Majuro", "Marshall Islands", "Pacific/Majuro", "🏝️"),
208
+ ("Palikir", "Micronesia", "Pacific/Pohnpei", "🐟"),
209
+ ("Yaren", "Nauru", "Pacific/Nauru", "🏝️"),
210
+ ("Funafuti", "Tuvalu", "Pacific/Funafuti", "🏝️"),
211
+ ("Tarawa", "Kiribati", "Pacific/Tarawa", "🐢"),
212
+ ("Avarua", "Cook Islands", "Pacific/Rarotonga", "🌺"),
213
+ ("Papeete", "French Polynesia", "Pacific/Tahiti", "🌺"),
214
+ ("Hagåtña", "Guam", "Pacific/Guam", "🌴"),
215
+ # Additional Major Cities and Missing Countries
216
+ # More US Cities
217
+ ("Atlanta", "USA", "America/New_York", "🍑"),
218
+ ("Portland", "USA", "America/Los_Angeles", "🌲"),
219
+ ("Minneapolis", "USA", "America/Chicago", "❄️"),
220
+ ("Nashville", "USA", "America/Chicago", "🎵"),
221
+ ("San Diego", "USA", "America/Los_Angeles", "🏄"),
222
+ ("Philadelphia", "USA", "America/New_York", "🔔"),
223
+ ("Houston", "USA", "America/Chicago", "🚀"),
224
+ ("Detroit", "USA", "America/New_York", "🚗"),
225
+ ("Tampa", "USA", "America/New_York", "🌴"),
226
+ ("New Orleans", "USA", "America/Chicago", "🎺"),
227
+ # Caribbean
228
+ ("Havana", "Cuba", "America/Havana", "🏝️"),
229
+ ("Kingston", "Jamaica", "America/Jamaica", "🎶"),
230
+ ("Port of Spain", "Trinidad and Tobago", "America/Port_of_Spain", "🌺"),
231
+ ("Bridgetown", "Barbados", "America/Barbados", "🏖️"),
232
+ ("Santo Domingo", "Dominican Republic", "America/Santo_Domingo", "🌴"),
233
+ ("San Juan", "Puerto Rico", "America/Puerto_Rico", "🏰"),
234
+ ("Nassau", "Bahamas", "America/Nassau", "🏝️"),
235
+ ("Georgetown", "Guyana", "America/Guyana", "🌊"),
236
+ ("Paramaribo", "Suriname", "America/Paramaribo", "🌴"),
237
+ ("Cayenne", "French Guiana", "America/Cayenne", "🐸"),
238
+ # More European Cities
239
+ ("Nice", "France", "Europe/Paris", "🌊"),
240
+ ("Toulouse", "France", "Europe/Paris", "✈️"),
241
+ ("Naples", "Italy", "Europe/Rome", "🍕"),
242
+ ("Florence", "Italy", "Europe/Rome", "🎨"),
243
+ ("Venice", "Italy", "Europe/Rome", "🚤"),
244
+ ("Seville", "Spain", "Europe/Madrid", "💃"),
245
+ ("Valencia", "Spain", "Europe/Madrid", "🍊"),
246
+ ("Cologne", "Germany", "Europe/Berlin", "⛪"),
247
+ ("Dresden", "Germany", "Europe/Berlin", "🏰"),
248
+ ("Salzburg", "Austria", "Europe/Vienna", "🎼"),
249
+ ("Basel", "Switzerland", "Europe/Zurich", "🏦"),
250
+ ("Malmö", "Sweden", "Europe/Stockholm", "🌉"),
251
+ ("Bergen", "Norway", "Europe/Oslo", "🐟"),
252
+ ("Aarhus", "Denmark", "Europe/Copenhagen", "🏛️"),
253
+ ("Thessaloniki", "Greece", "Europe/Athens", "🏺"),
254
+ ("Porto", "Portugal", "Europe/Lisbon", "🍷"),
255
+ ("Krakow", "Poland", "Europe/Warsaw", "🐲"),
256
+ ("Split", "Croatia", "Europe/Zagreb", "⛵"),
257
+ ("Ljubljana", "Slovenia", "Europe/Ljubljana", "🏰"),
258
+ ("Bratislava", "Slovakia", "Europe/Bratislava", "🏰"),
259
+ # Missing Asian Countries
260
+ ("Vientiane", "Laos", "Asia/Vientiane", "🛕"),
261
+ ("Bandar Seri Begawan", "Brunei", "Asia/Brunei", "🕌"),
262
+ ("Dili", "East Timor", "Asia/Dili", "🌴"),
263
+ ("Male", "Maldives", "Indian/Maldives", "🏝️"),
264
+ # Central Asia
265
+ ("Nur-Sultan", "Kazakhstan", "Asia/Almaty", "🏙️"),
266
+ ("Astana", "Kazakhstan", "Asia/Almaty", "🌟"),
267
+ # More Middle East
268
+ ("Isfahan", "Iran", "Asia/Tehran", "🕌"),
269
+ ("Shiraz", "Iran", "Asia/Tehran", "🌹"),
270
+ ("Aleppo", "Syria", "Asia/Damascus", "🏛️"),
271
+ ("Basra", "Iraq", "Asia/Baghdad", "🌊"),
272
+ ("Erbil", "Iraq", "Asia/Baghdad", "🏛️"),
273
+ # More African Cities
274
+ ("Marrakech", "Morocco", "Africa/Casablanca", "🕌"),
275
+ ("Fez", "Morocco", "Africa/Casablanca", "🎨"),
276
+ ("Alexandria", "Egypt", "Africa/Cairo", "🏺"),
277
+ ("Luxor", "Egypt", "Africa/Cairo", "🏛️"),
278
+ ("Kano", "Nigeria", "Africa/Lagos", "🎭"),
279
+ ("Ibadan", "Nigeria", "Africa/Lagos", "🌆"),
280
+ ("Durban", "South Africa", "Africa/Johannesburg", "🏄"),
281
+ ("Pretoria", "South Africa", "Africa/Johannesburg", "🏛️"),
282
+ ("Port Elizabeth", "South Africa", "Africa/Johannesburg", "🌊"),
283
+ ("Mombasa", "Kenya", "Africa/Nairobi", "🏖️"),
284
+ ("Kisumu", "Kenya", "Africa/Nairobi", "🐟"),
285
+ ("Arusha", "Tanzania", "Africa/Dar_es_Salaam", "🦁"),
286
+ ("Zanzibar", "Tanzania", "Africa/Dar_es_Salaam", "🏝️"),
287
+ ("Entebbe", "Uganda", "Africa/Kampala", "✈️"),
288
+ ("Kigali", "Rwanda", "Africa/Kigali", "🦍"),
289
+ ("Bujumbura", "Burundi", "Africa/Bujumbura", "🌋"),
290
+ ("Djibouti", "Djibouti", "Africa/Djibouti", "🌊"),
291
+ ("Asmara", "Eritrea", "Africa/Asmara", "⛰️"),
292
+ ("Mogadishu", "Somalia", "Africa/Mogadishu", "🏖️"),
293
+ ("N'Djamena", "Chad", "Africa/Ndjamena", "🌵"),
294
+ ("Bangui", "Central African Republic", "Africa/Bangui", "🌳"),
295
+ ("Yaoundé", "Cameroon", "Africa/Douala", "🌳"),
296
+ ("Libreville", "Gabon", "Africa/Libreville", "🌳"),
297
+ ("Malabo", "Equatorial Guinea", "Africa/Malabo", "🏝️"),
298
+ ("São Tomé", "São Tomé and Príncipe", "Africa/Sao_Tome", "🌺"),
299
+ ("Kinshasa", "Democratic Republic of Congo", "Africa/Kinshasa", "🌊"),
300
+ ("Brazzaville", "Republic of Congo", "Africa/Brazzaville", "🌊"),
301
+ ("Bamako", "Mali", "Africa/Bamako", "🐪"),
302
+ ("Ouagadougou", "Burkina Faso", "Africa/Ouagadougou", "🌾"),
303
+ ("Niamey", "Niger", "Africa/Niamey", "🌵"),
304
+ ("Conakry", "Guinea", "Africa/Conakry", "🌊"),
305
+ ("Freetown", "Sierra Leone", "Africa/Freetown", "⛰️"),
306
+ ("Monrovia", "Liberia", "Africa/Monrovia", "🌊"),
307
+ ("Lomé", "Togo", "Africa/Lome", "🏖️"),
308
+ ("Porto-Novo", "Benin", "Africa/Porto-Novo", "🌴"),
309
+ ("Nouakchott", "Mauritania", "Africa/Nouakchott", "🐪"),
310
+ ("Bissau", "Guinea-Bissau", "Africa/Bissau", "🌊"),
311
+ ("Praia", "Cape Verde", "Atlantic/Cape_Verde", "🌊"),
312
+ ("Maseru", "Lesotho", "Africa/Maseru", "⛰️"),
313
+ ("Mbabane", "Eswatini", "Africa/Mbabane", "🏔️"),
314
+ ("Moroni", "Comoros", "Indian/Comoro", "🌺"),
315
+ ("Victoria", "Seychelles", "Indian/Mahe", "🏝️"),
316
+ # Pacific Islands and Oceania
317
+ ("Honolulu", "USA", "Pacific/Honolulu", "🌺"),
318
+ ("Anchorage", "USA", "America/Anchorage", "🐻"),
319
+ ("Fairbanks", "USA", "America/Anchorage", "❄️"),
320
+ ("Hobart", "Australia", "Australia/Hobart", "🌿"),
321
+ ("Gold Coast", "Australia", "Australia/Brisbane", "🏄"),
322
+ ("Newcastle", "Australia", "Australia/Sydney", "⚓"),
323
+ ("Wollongong", "Australia", "Australia/Sydney", "🌊"),
324
+ ("Dunedin", "New Zealand", "Pacific/Auckland", "🐧"),
325
+ ("Hamilton", "New Zealand", "Pacific/Auckland", "🐄"),
326
+ ("Tauranga", "New Zealand", "Pacific/Auckland", "🏖️"),
327
+ ]
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtime
3
+ Version: 0.3.0
4
+ Summary: Global Time Utility (gtime) - A modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode
5
+ Author-email: Savitoj Singh <savv@duck.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/savitojs/gtime
8
+ Project-URL: Repository, https://github.com/savitojs/gtime
9
+ Project-URL: Bug Tracker, https://github.com/savitojs/gtime/issues
10
+ Project-URL: Documentation, https://github.com/savitojs/gtime#readme
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: rich
15
+ Requires-Dist: python-dateutil
16
+ Requires-Dist: thefuzz
17
+ Requires-Dist: pytz; python_version < "3.9"
18
+ Dynamic: license-file
19
+
20
+ # gtime 🌐
21
+
22
+ gtime (Global Time) is a modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode
23
+
24
+ ## Features
25
+ - Fast city lookup with fuzzy search and suggestions
26
+ - Add/remove/list favorite cities
27
+ - Compare times for multiple cities
28
+ - Meeting time conversion across favorites
29
+ - Live/watch mode for real-time updates
30
+ - Colorful, user-friendly output (using Rich)
31
+ - Comprehensive test suite (pytest)
32
+ - Performance-optimized for large city databases
33
+
34
+ ## Installation (from source)
35
+ Clone the repo and install locally:
36
+
37
+ ```sh
38
+ pip install .
39
+ ```
40
+
41
+ Or, install from PyPI:
42
+
43
+ ```sh
44
+ pip install gtime
45
+ ```
46
+ ## Demo
47
+
48
+ **Note:** Some command output may appear broken in the demo, but it works correctly in real use
49
+
50
+ ![demo](./assets/demo.gif)
51
+
52
+ ## Usage
53
+ After installation, run the CLI:
54
+
55
+ ```sh
56
+ gtime [command] [arguments]
57
+ ```
58
+
59
+ Or as a module:
60
+
61
+ ```sh
62
+ python -m gtime.cli [command] [arguments]
63
+ ```
64
+
65
+ Example commands:
66
+ - `gtime London` — Show time for London
67
+ - `gtime add Tokyo` — Add Tokyo to favorites
68
+ - `gtime list` — List favorite cities
69
+ - `gtime compare London Tokyo` — Compare cities
70
+ - `gtime meeting at 10:00 AM` — Meeting time conversion
71
+ - `gtime watch` — Live mode
72
+
73
+ ## Development & Publishing
74
+
75
+ ### GitHub Actions
76
+ This project includes automated workflows:
77
+ - **Tests**: Runs on every push/PR across Python 3.8-3.12
78
+ - **Publish**: Automatically publishes to PyPI upon new GitHub release
79
+
80
+ ## License
81
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ gtime/__init__.py
5
+ gtime/cli.py
6
+ gtime/core.py
7
+ gtime/data.py
8
+ gtime.egg-info/PKG-INFO
9
+ gtime.egg-info/SOURCES.txt
10
+ gtime.egg-info/dependency_links.txt
11
+ gtime.egg-info/entry_points.txt
12
+ gtime.egg-info/requires.txt
13
+ gtime.egg-info/top_level.txt
14
+ tests/test_gtime.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gtime = gtime.cli:main
@@ -0,0 +1,6 @@
1
+ rich
2
+ python-dateutil
3
+ thefuzz
4
+
5
+ [:python_version < "3.9"]
6
+ pytz
@@ -0,0 +1 @@
1
+ gtime
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gtime"
7
+ version = "0.3.0"
8
+ description = "Global Time Utility (gtime) - A modern, colorful Python CLI utility for global time zone lookup, comparison, and management. It supports fuzzy search, favorites, city comparison, meeting time conversion, and a live/watch mode"
9
+ authors = [
10
+ { name = "Savitoj Singh", email = "savv@duck.com" }
11
+ ]
12
+ readme = "README.md"
13
+ license = { text = "MIT" }
14
+ requires-python = ">=3.7"
15
+ dependencies = [
16
+ "rich",
17
+ "python-dateutil",
18
+ "thefuzz",
19
+ "pytz; python_version < '3.9'"
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/savitojs/gtime"
24
+ Repository = "https://github.com/savitojs/gtime"
25
+ "Bug Tracker" = "https://github.com/savitojs/gtime/issues"
26
+ Documentation = "https://github.com/savitojs/gtime#readme"
27
+
28
+ [project.scripts]
29
+ gtime = "gtime.cli:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ include = ["gtime*"]
33
+ exclude = ["perf*", "assets*"]
gtime-0.3.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ import subprocess
4
+ import sys
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+ import pytest
9
+
10
+ SCRIPT = "gtime" # Entry point for the CLI
11
+
12
+ FAV_FILE = Path.home() / ".gtime_favorites.json"
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def cleanup_favs():
16
+ # Remove favorites file before and after each test
17
+ if FAV_FILE.exists():
18
+ FAV_FILE.unlink()
19
+ yield
20
+ if FAV_FILE.exists():
21
+ FAV_FILE.unlink()
22
+
23
+ def run_cli(*args):
24
+ env = os.environ.copy()
25
+ env["PYTHONIOENCODING"] = "utf-8"
26
+ env["PYTHONPATH"] = os.path.abspath(os.path.dirname(__file__))
27
+ # Always run in the workspace root so .gtime_favorites.json is consistent
28
+ result = subprocess.run([SCRIPT, *args], capture_output=True, text=True, cwd=os.path.dirname(__file__), env=env)
29
+ return result
30
+
31
+ def test_help():
32
+ out = run_cli("-h")
33
+ assert "Global Time" in out.stdout
34
+ assert "Usage" in out.stdout
35
+
36
+ def test_add_and_list_favorite():
37
+ run_cli("add", "London")
38
+ out = run_cli("list")
39
+ assert "London" in out.stdout
40
+ assert "UK" in out.stdout
41
+
42
+ def test_remove_favorite():
43
+ run_cli("add", "London")
44
+ run_cli("remove", "London")
45
+ out = run_cli("list")
46
+ assert "London" not in out.stdout
47
+
48
+ def test_city_lookup():
49
+ out = run_cli("Tokyo")
50
+ assert "Tokyo" in out.stdout
51
+ assert "Japan" in out.stdout
52
+
53
+ def test_fuzzy_search():
54
+ out = run_cli("Londn") # typo
55
+ assert "London" in out.stdout or "Did you mean" in out.stdout
56
+
57
+ def test_compare():
58
+ out = run_cli("compare", "London", "Tokyo")
59
+ assert "London" in out.stdout and "Tokyo" in out.stdout
60
+
61
+ def test_meeting_time():
62
+ out = run_cli("meeting", "at", "10:00", "AM")
63
+ assert "Favorite Cities" in out.stdout or "No favorite cities" in out.stdout
64
+
65
+ def test_invalid_city():
66
+ out = run_cli("NotACity")
67
+ assert "Invalid command" in out.stdout or "Did you mean" in out.stdout
68
+
69
+ def test_no_favorites():
70
+ out = run_cli("list")
71
+ assert "No favorite cities" in out.stdout