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 +21 -0
- gtime-0.3.0/PKG-INFO +81 -0
- gtime-0.3.0/README.md +62 -0
- gtime-0.3.0/gtime/__init__.py +1 -0
- gtime-0.3.0/gtime/cli.py +322 -0
- gtime-0.3.0/gtime/core.py +224 -0
- gtime-0.3.0/gtime/data.py +327 -0
- gtime-0.3.0/gtime.egg-info/PKG-INFO +81 -0
- gtime-0.3.0/gtime.egg-info/SOURCES.txt +14 -0
- gtime-0.3.0/gtime.egg-info/dependency_links.txt +1 -0
- gtime-0.3.0/gtime.egg-info/entry_points.txt +2 -0
- gtime-0.3.0/gtime.egg-info/requires.txt +6 -0
- gtime-0.3.0/gtime.egg-info/top_level.txt +1 -0
- gtime-0.3.0/pyproject.toml +33 -0
- gtime-0.3.0/setup.cfg +4 -0
- gtime-0.3.0/tests/test_gtime.py +71 -0
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
|
+

|
|
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
|
+

|
|
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
|
gtime-0.3.0/gtime/cli.py
ADDED
|
@@ -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
|
+

|
|
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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|