├── tests ├── __init__.py ├── conftest.py └── test_utils.py ├── timezones_cli ├── __init__.py ├── utils │ ├── variables.py │ ├── validators.py │ ├── abbreviations.py │ └── __init__.py ├── main.py └── commands │ ├── __init__.py │ ├── select.py │ ├── remove.py │ ├── utc.py │ ├── show.py │ ├── search.py │ └── add.py ├── mypy.ini ├── .dockerignore ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── main.yaml │ ├── release.yml │ └── image_release.yml ├── Dockerfile ├── LICENCE ├── CONTRIBUTING.md ├── Makefile ├── pyproject.toml ├── .gitignore ├── README.md └── uv.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /timezones_cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any=True 3 | warn_redundant_casts=True 4 | warn_unused_ignores=True 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.egg-info 3 | build/ 4 | dist/ 5 | env/ 6 | .env/ 7 | tests/ 8 | venv/ 9 | .venv/ 10 | -------------------------------------------------------------------------------- /timezones_cli/utils/variables.py: -------------------------------------------------------------------------------- 1 | """ Global variables """ 2 | 3 | from pathlib import Path 4 | 5 | home_dir: str = str(Path.home()) 6 | filename: str = ".tz-cli" 7 | config_file: str = f"{home_dir}/{filename}" 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.1.2 4 | hooks: 5 | - id: ruff 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.5.0 8 | hooks: 9 | - id: check-byte-order-marker 10 | - id: trailing-whitespace 11 | - id: end-of-file-fixer 12 | -------------------------------------------------------------------------------- /timezones_cli/main.py: -------------------------------------------------------------------------------- 1 | """ Entrypoint of the CLI """ 2 | 3 | import click 4 | 5 | from timezones_cli.commands import add, remove, search, select, show, utc 6 | 7 | 8 | @click.group() 9 | def cli(): 10 | pass 11 | 12 | 13 | cli.add_command(add) 14 | cli.add_command(remove) 15 | cli.add_command(search) 16 | cli.add_command(select) 17 | cli.add_command(show) 18 | cli.add_command(utc) 19 | -------------------------------------------------------------------------------- /timezones_cli/commands/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exports for CLI commands. 3 | """ 4 | 5 | # flake8: noqa 6 | from timezones_cli.commands.add import add 7 | from timezones_cli.commands.remove import remove 8 | from timezones_cli.commands.search import search 9 | from timezones_cli.commands.select import select 10 | from timezones_cli.commands.show import show 11 | from timezones_cli.commands.utc import utc 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | types: 8 | - opened 9 | jobs: 10 | Linter: 11 | runs-on: ubuntu-latest 12 | if: github.event_name == 'pull_request' 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: 3.8 19 | - name: Ruff Check Linting 20 | run: | 21 | pip install ruff 22 | ruff check . 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ pytest fixtures """ 2 | from collections import namedtuple 3 | 4 | import pytest 5 | 6 | 7 | @pytest.fixture(scope="function") 8 | def country_data(): 9 | Country = namedtuple( 10 | "Country", ["alpha_2", "alpha_3", "name", "numeric", "official_name"] 11 | ) 12 | 13 | return [ 14 | Country( 15 | alpha_2="NP", 16 | alpha_3="NPL", 17 | name="Nepal", 18 | numeric="524", 19 | official_name="Federal Democratic Republic of Nepal", 20 | ) 21 | ] 22 | -------------------------------------------------------------------------------- /timezones_cli/utils/validators.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | class TzAbbrev(click.ParamType): 5 | name = "Timezone Abbreviation" 6 | 7 | def convert(self, value: str, param: str, ctx) -> str: 8 | # validate timezone abbreviation length 9 | if all(c not in value for c in ["+", "-"]) and ctx.params.get("zone"): 10 | if 2 <= len(value) <= 4: 11 | return value.upper() 12 | else: 13 | click.echo(ctx.command.get_help(ctx)) 14 | raise click.BadParameter( 15 | "timezone code needs to be between 2 to 4 letters when using --zone flag" 16 | ) 17 | return value 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Publisher 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | Publish-to-PyPI: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Install Dependencies 19 | run: pip install wheel 20 | - 21 | name: Install uv 22 | uses: astral-sh/setup-uv@v5 23 | with: 24 | version: "0.6.2" 25 | - 26 | name: Build Distribution packages 27 | run: uv build --wheel 28 | - 29 | name: Publish a Python distribution to PyPI 30 | uses: pypa/gh-action-pypi-publish@release/v1 31 | if: startsWith(github.ref, 'refs/tags/') 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.PYPI_PASSWORD }} 35 | -------------------------------------------------------------------------------- /timezones_cli/commands/select.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sub command to select timezone from config file. 3 | """ 4 | 5 | import click 6 | 7 | from timezones_cli.utils import console, get_local_time, handle_interaction, variables 8 | 9 | 10 | @click.command() 11 | @click.option( 12 | "--toggle", 13 | "-t", 14 | help="Toggle for 24 hours format", 15 | type=bool, 16 | default=False, 17 | is_flag=True, 18 | ) 19 | def select(toggle: bool): 20 | """ 21 | Interactively select the timezone from your config file to get local time. 22 | 23 | $ tz select 24 | """ 25 | config_file = variables.config_file 26 | 27 | with open(config_file, "r+") as file: 28 | data = [line.rstrip() for line in file] 29 | 30 | if not data: 31 | return console.print("Config file contains no timezone:x:") 32 | 33 | entries = handle_interaction(data) 34 | 35 | return get_local_time(entries, toggle=toggle) 36 | -------------------------------------------------------------------------------- /timezones_cli/commands/remove.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sub command to remove saved timezones from config file. 3 | """ 4 | 5 | from typing import Optional 6 | 7 | import click 8 | 9 | from timezones_cli.utils import ( 10 | console, 11 | print_help_msg, 12 | remove_timezone, 13 | validate_timezone, 14 | ) 15 | 16 | 17 | @click.command() 18 | @click.option("--name", "-n", help="Name of the timezone", type=str) 19 | @click.option( 20 | "--interactive", 21 | "-i", 22 | help="Delete timezones in interactive mode.", 23 | is_flag=True, 24 | ) 25 | def remove(name: Optional[str], interactive: bool): 26 | """ 27 | Remove timezone to the config file. 28 | 29 | $ tz remove "Asia/Kolkata" 30 | 31 | $ tz remove -i 32 | """ 33 | exists = interactive or name 34 | 35 | if not exists: 36 | return print_help_msg(remove) 37 | 38 | if name and interactive: 39 | return console.print("Cannot use both flags at the same time.:x:") 40 | 41 | if name: 42 | validate_timezone(name) 43 | 44 | remove_timezone(interactive, name) 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/python:3.11.11-slim-bookworm AS build 2 | 3 | ENV UV_PROJECT_ENVIRONMENT=/home/tz/.venv 4 | ENV UV_COMPILE_BYTECODE=1 5 | ENV UV_LINK_MODE=copy 6 | 7 | RUN < /dev/null)" = "" ]; then \ 16 | $(MAKE) -s build; \ 17 | fi 18 | 19 | run: build.if 20 | @docker run --rm -it -v ${HOME}/.tz-cli:/home/tz/.tz-cli timezones-cli $(cmd) 21 | 22 | clean: # Clean temporary files 23 | @rm -rf $(TMP_PATH) __pycache__ .pytest_cache 24 | @find . -name '*.pyc' -delete 25 | @find . -name '__pycache__' -delete 26 | @rm -rf build dist 27 | 28 | test: # Run pytest 29 | @pytest -vvv 30 | 31 | format: # Format using black 32 | @black . 33 | 34 | check: # Check for formatting issues using black 35 | @black --check --diff . 36 | 37 | 38 | setup: # Initial project setup 39 | @echo "Creating virtual env at: $(VENV_DIR)"s 40 | @python3 -m venv $(VENV_DIR) 41 | @echo "Installing dependencies..." 42 | @source $(VENV_DIR)/bin/activate && pip install -e . 43 | @echo -e "\n✅ Done.\n🎉 Run the following commands to get started:\n\n ➡️ source $(VENV_DIR)/bin/activate\n ➡️ tz \n" 44 | 45 | help: # Show this help 46 | @egrep -h '\s#\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 47 | -------------------------------------------------------------------------------- /timezones_cli/commands/utc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sub command to get UTC times 3 | """ 4 | 5 | import sys 6 | 7 | import click 8 | 9 | from timezones_cli.utils import ( 10 | console, 11 | get_local_utc_time, 12 | get_utc_time, 13 | validate_time, 14 | validate_timezone, 15 | ) 16 | 17 | 18 | @click.command() 19 | @click.argument("time", required=False) 20 | @click.argument("timezone", required=False) 21 | def utc(time, timezone): 22 | """ 23 | Convert a specific time from any timezone to UTC. 24 | 25 | > Hours are calculated in 24 hours format. You can specify 'AM' or 'PM' if you are using 12 hours format. 26 | 27 | EXAMPLE: \n 28 | 29 | $ tz utc # show current system time in UTC. 30 | 31 | $ tz utc "8:15" "Asia/Kathmandu" # will be evaluated as AM, following the 24 hour format. 32 | 33 | $ tz utc "20:15" "Asia/Kathmandu" # will be evaluated as PM despite any suffix, following the 24 hour format. 34 | 35 | $ tz utc "8:15 PM" "Asia/Kathmandu" # will be evaluated as specified. 36 | """ 37 | if not time or not timezone: 38 | get_local_utc_time() 39 | sys.exit() 40 | 41 | try: 42 | validate_timezone(timezone) 43 | hour, minute, time_suffix = validate_time(time) 44 | except Exception: 45 | console.print("[bold red]:x:Invalid input value[/bold red]") 46 | sys.exit(0) 47 | 48 | time = f"{str(hour).zfill(2)}:{str(minute).zfill(2)} {time_suffix}" 49 | 50 | return get_utc_time(hour, minute, timezone, time) 51 | -------------------------------------------------------------------------------- /timezones_cli/commands/show.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sub command to show datetime from saved timezones. 3 | """ 4 | 5 | import sys 6 | from typing import List, Union 7 | 8 | import click 9 | 10 | from timezones_cli.utils import check_config as check_configuration 11 | from timezones_cli.utils import ( 12 | console, 13 | get_local_time, 14 | get_system_time, 15 | handle_interaction, 16 | ) 17 | 18 | 19 | @click.command() 20 | @click.option( 21 | "--interactive", 22 | "-i", 23 | help="Delete timezones in interactive mode.", 24 | is_flag=True, 25 | ) 26 | @click.option( 27 | "--toggle", 28 | "-t", 29 | help="Toggle for 24 hours format", 30 | type=bool, 31 | default=False, 32 | is_flag=True, 33 | ) 34 | def show(interactive: bool, toggle: bool): 35 | """ 36 | Show time based on the defaults at .tz-cli file. 37 | 38 | $ tz show 39 | """ 40 | timezone_data: Union[List, bool] = check_configuration() 41 | 42 | if not timezone_data: 43 | console.print( 44 | "File is empty or No configuration file is present in your system.:x:\n", 45 | style="bold red", 46 | ) 47 | console.print( 48 | "Use `tz add` to create and add timezone to your config file.:memo:\n", 49 | style="bold green", 50 | ) 51 | 52 | console.print( 53 | f"Your system datetime is: {get_system_time()}", 54 | style="bold yellow", 55 | ) 56 | sys.exit() 57 | 58 | if interactive: 59 | timezone_data = handle_interaction(timezone_data) 60 | 61 | return get_local_time(timezone_data, toggle=toggle) 62 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=75.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "timezones_cli" 7 | version = "0.3.8" 8 | description = "Get local datetime from multiple timezones!" 9 | readme = "README.md" 10 | license = { text = "MIT" } 11 | authors = [ 12 | { name = "Yankee Maharjan" } 13 | ] 14 | urls = { homepage = "https://github.com/yankeexe/timezones-cli" } 15 | classifiers = [ 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Operating System :: OS Independent", 20 | "License :: OSI Approved :: MIT License", 21 | ] 22 | dependencies = [ 23 | "click==8.1.5", 24 | "tabulate==0.9.0", 25 | "rich<=7.1.0", 26 | "pycountry==22.3.5", 27 | "pytz", 28 | "simple-term-menu==1.6.1", 29 | "tzlocal==2.1", 30 | "thefuzz[speedup]", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "pytest>=6.2.5", 36 | "black<=20.8b1", 37 | "pre-commit", 38 | "mypy", 39 | "freezegun", 40 | "flake8", 41 | "types-pytz", 42 | "types-tzlocal", 43 | "types-tabulate", 44 | ] 45 | 46 | [project.scripts] 47 | tz = "timezones_cli.main:cli" 48 | 49 | [tool.setuptools] 50 | packages = [ 51 | "timezones_cli", 52 | "timezones_cli.commands", 53 | "timezones_cli.utils" 54 | ] 55 | 56 | [tool.setuptools.package-data] 57 | "*" = ["py.typed"] 58 | 59 | [tool.setuptools.exclude-package-data] 60 | "*" = ["tests/*", "dist/*", "build/*", "*.egg-info/*"] 61 | 62 | [dependency-groups] 63 | build = [ 64 | "setuptools>=75.8.0", 65 | ] 66 | -------------------------------------------------------------------------------- /.github/workflows/image_release.yml: -------------------------------------------------------------------------------- 1 | name: Publish CLI Docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | id-token: write 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Install cosign 24 | uses: sigstore/cosign-installer@v3.8.0 25 | 26 | - name: Cosign version 27 | run: cosign version 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | 32 | - name: Setup Docker buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | - name: Login to GitHub Container Registry 36 | uses: docker/login-action@v3 37 | with: 38 | registry: ${{ env.REGISTRY }} 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Extract metadata (tags, labels) for Docker 43 | id: meta 44 | uses: docker/metadata-action@v5 45 | with: 46 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 47 | 48 | - name: Build and push Docker image 49 | id: push-step 50 | uses: docker/build-push-action@v3 51 | with: 52 | context: . 53 | platforms: linux/amd64,linux/arm64 54 | push: true 55 | tags: ${{ steps.meta.outputs.tags }} 56 | labels: ${{ steps.meta.outputs.labels }} 57 | 58 | - name: Sign the container image 59 | run: cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.push-step.outputs.digest }} 60 | -------------------------------------------------------------------------------- /timezones_cli/commands/search.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | import click 4 | 5 | from timezones_cli.utils import ( 6 | console, 7 | get_local_time, 8 | handle_interaction, 9 | query_handler, 10 | tz_abbreviation_handler, 11 | ) 12 | from timezones_cli.utils.validators import TzAbbrev 13 | 14 | 15 | @click.command() 16 | @click.argument("query", type=TzAbbrev()) 17 | @click.option( 18 | "--zone", 19 | "-z", 20 | help="define timezone short codes", 21 | is_flag=True, 22 | ) 23 | @click.option( 24 | "--toggle", 25 | "-t", 26 | help="Toggle for 24 hours format", 27 | is_flag=True, 28 | ) 29 | def search(query: str, zone: bool, toggle: bool): 30 | """ 31 | Get time based on the entered timezone or country code 32 | 33 | - using country code (either 2 or 3 letters): 34 | 35 | $ tz search US 36 | 37 | $ tz search USA 38 | 39 | - using timezone: 40 | 41 | $ tz search Asia/Kathmandu 42 | 43 | - using fuzzy text: (example: Ireland) 44 | 45 | $ tz search Irela 46 | 47 | - using timezone shortcodes (--zone or -z flag): 48 | 49 | $ tz search pst -z 50 | 51 | $ tz search ist -z 52 | 53 | $ tz search jst -z 54 | 55 | $ tz search cest -z 56 | 57 | $ tz search +0545 -z 58 | 59 | $ tz search +05 -z 60 | """ 61 | try: 62 | if zone: 63 | result = tz_abbreviation_handler(query) 64 | else: 65 | result = query_handler(query) 66 | 67 | # If length is greater than one, show terminal menu. 68 | if isinstance(result, t.List) and len(result) > 1: 69 | result = handle_interaction(result) 70 | return get_local_time(result, query, toggle=toggle) 71 | except LookupError: 72 | return console.print( 73 | "Couldn't resolve your query, please try other keywords.:x:" 74 | ) 75 | -------------------------------------------------------------------------------- /timezones_cli/commands/add.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sub command to add timezones for quick glance. 3 | """ 4 | 5 | from typing import List 6 | 7 | import click 8 | 9 | from timezones_cli.utils import ( 10 | check_config, 11 | console, 12 | handle_interaction, 13 | query_handler, 14 | variables, 15 | get_local_time, 16 | ) 17 | 18 | 19 | @click.command() 20 | @click.argument("query") 21 | def add(query: str): 22 | """ 23 | Add timezone to the config file. 24 | """ 25 | added_timezones = [] 26 | added_timezones_raw = [] 27 | existing_timezones = [] 28 | existing_timezones_raw = [] 29 | line_break = "\n" 30 | 31 | try: 32 | timezones = query_handler(query) 33 | except LookupError: 34 | return console.print( 35 | "Couldn't resolve your query, please try other keywords.:x:" 36 | ) 37 | 38 | if len(timezones) > 1: 39 | timezones = handle_interaction(timezones) 40 | 41 | config_file = variables.config_file 42 | 43 | if not check_config(): 44 | with open(config_file, "w+"): 45 | pass 46 | 47 | with open(config_file, "r+") as file: 48 | data: List = [line.rstrip() for line in file] 49 | 50 | for timezone in timezones: 51 | if timezone in data: 52 | existing_timezones.append( 53 | f"[bold green]{timezone}:white_check_mark:[/bold green]" 54 | ) 55 | existing_timezones_raw.append(timezone) 56 | continue 57 | 58 | file.read() 59 | # Add to the end of the file. 60 | file.write(f"{timezone}\n") 61 | added_timezones_raw.append(timezone) 62 | added_timezones.append( 63 | f"[bold blue]{timezone}[/bold blue] :white_check_mark:" 64 | ) 65 | 66 | if existing_timezones: 67 | console.print( 68 | f"[bold blue]🌐 Timezone/s already exists![/bold blue]\n{line_break.join(existing_timezones)}" 69 | ) 70 | return get_local_time(existing_timezones_raw) 71 | 72 | if added_timezones: 73 | console.print( 74 | f"[bold green]New timezone/s added successfully:[/bold green]\n{line_break.join(added_timezones)}" 75 | ) 76 | return get_local_time(added_timezones_raw) 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 4 | 5 | ### Python ### 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | pytestdebug.log 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | db.sqlite3 68 | db.sqlite3-journal 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | doc/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # IPython 88 | profile_default/ 89 | ipython_config.py 90 | 91 | # pyenv 92 | .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | pythonenv* 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # profiling data 143 | .prof 144 | 145 | # IDE 146 | .vscode 147 | .idea 148 | .vim 149 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """ Tests for utils """ 2 | from unittest.mock import patch 3 | 4 | import pytest 5 | import pytz 6 | from freezegun import freeze_time 7 | from tabulate import tabulate 8 | 9 | from timezones_cli import utils 10 | 11 | 12 | @pytest.mark.usefixtures("country_data") 13 | def test_extract_fuzzy_country_data(country_data): 14 | """ 15 | Test the extraction of data returned by fuzzy search. 16 | """ 17 | 18 | assert utils.extract_fuzzy_country_data(country_data) == ( 19 | "Nepal", 20 | "Federal Democratic Republic of Nepal", 21 | "NP", 22 | "NPL", 23 | ) 24 | 25 | 26 | def test_get_timezones(): 27 | """ 28 | Test the timezone names returned with search query. 29 | """ 30 | assert utils.get_timezones("np") == ["Asia/Kathmandu"] 31 | assert utils.get_timezones("in") == ["Asia/Kolkata"] 32 | 33 | 34 | def test_get_timezones_failure(): 35 | """ 36 | Test for system exit when bad search query is provided. 37 | """ 38 | with pytest.raises(SystemExit): 39 | assert utils.get_timezones("fail") 40 | 41 | with pytest.raises(SystemExit): 42 | assert utils.get_timezones("xz") 43 | 44 | 45 | def test_validate_timezone(): 46 | """ 47 | Tests for valid timezone. 48 | """ 49 | assert utils.validate_timezone("Asia/Kathmandu") is True 50 | assert utils.validate_timezone("Asia/Kolkata") is True 51 | 52 | 53 | def test_validate_timezone_failure(): 54 | """ 55 | Tests for invalid timezones. 56 | """ 57 | with pytest.raises(SystemExit): 58 | assert utils.validate_timezone("Python/Tests") 59 | assert utils.validate_timezone("Asia/Catmandu") 60 | 61 | 62 | class TestMatchFuzzy: 63 | def test_match_fuzzy_match(self): 64 | query = "Europe" 65 | result = utils.match_fuzzy(query) 66 | assert result is not None 67 | 68 | def test_match_fuzzy_no_match(self): 69 | query = "Nepal" 70 | from timezones_cli.utils import match_fuzzy 71 | 72 | result = match_fuzzy(query) 73 | assert len(result) == 0 74 | 75 | 76 | class TestQueryHandler: 77 | def test_query_handler_use_fuzzy_match(self): 78 | query = "Africa" 79 | result = utils.query_handler(query) 80 | assert "Africa/Asmara" in result 81 | 82 | def test_query_handler_user_fuzzy_search(self): 83 | query = "np" 84 | result = utils.query_handler(query) 85 | assert result == ["Asia/Kathmandu"] 86 | 87 | 88 | @patch("timezones_cli.utils.console") 89 | @patch("timezones_cli.utils.tzlocal.get_localzone", autospec=True) 90 | def test_get_local_utc_time(mock_localzone, mock_console): 91 | headers = ["Time Zone", "Local Time", "UTC Time"] 92 | mock_localzone.return_value = pytz.timezone("Asia/Kathmandu") 93 | 94 | with freeze_time("2022-01-26 07:18:30"): 95 | utils.get_local_utc_time() 96 | 97 | mock_console.print.assert_called_once_with( 98 | tabulate( 99 | [("Asia/Kathmandu", "07:18 AM", "07:18 AM")], 100 | headers, 101 | tablefmt="fancy_grid", 102 | ) 103 | ) 104 | -------------------------------------------------------------------------------- /timezones_cli/utils/abbreviations.py: -------------------------------------------------------------------------------- 1 | TIMEZONES = { 2 | "ET": "Eastern Standard Time", 3 | "CT": "Central Standard Time", 4 | "MT": "Mountain Standard Time", 5 | "PT": "Pacific Standard Time", 6 | "AK": "Alaskan Standard Time", 7 | "HAST": "Hawaiian Standard Time", 8 | "MST": "Mountain Standard Time", 9 | "AST": "Atlantic Standard Time", 10 | "MOST": "Morocco Standard Time", 11 | "UTC": "UTC", 12 | "GMT": "GMT Standard Time", 13 | "GST": "Greenwich Standard Time", 14 | "WET": "West Europe Standard Time", 15 | "CET": "Central Europe Standard Time", 16 | "RST": "Romance Standard Time", 17 | "CEST": "Central European Standard Time", 18 | "ECT": "W. Central Africa Standard Time", 19 | "JST": "Japan Standard Time", 20 | "GTBST": "GTB Standard Time", 21 | "MEST": "Middle East Standard Time", 22 | "EGST": "Egypt Standard Time", 23 | "SST": "Syria Standard Time", 24 | "SAST": "South Africa Standard Time", 25 | "EET": "FLE Standard Time", 26 | "ISST": "Israel Standard Time", 27 | "EEST": "E. Europe Standard Time", 28 | "NMST": "Namibia Standard Time", 29 | "ARST": "Arabic Standard Time", 30 | "ABST": "Arab Standard Time", 31 | "MSK": "Russian Standard Time", 32 | "EAT": "E. Africa Standard Time", 33 | "IRST": "Iran Standard Time", 34 | "ARBST": "Arabian Standard Time", 35 | "AZT": "Azerbaijan Standard Time", 36 | "MUT": "Mauritius Standard Time", 37 | "GET": "Georgian Standard Time", 38 | "AMT": "Caucasus Standard Time", 39 | "AFT": "Afghanistan Standard Time", 40 | "YEKT": "Ekaterinburg Standard Time", 41 | "PKT": "Pakistan Standard Time", 42 | "WAST": "West Asia Standard Time", 43 | "IST": "India Standard Time", 44 | "SLT": "Sri Lanka Standard Time", 45 | "NPT": "Nepal Standard Time", 46 | "BTT": "Central Asia Standard Time", 47 | "BST": "Bangladesh Standard Time", 48 | "NCAST": "N. Central Asia Standard Time", 49 | "MYST": "Myanmar Standard Time", 50 | "THA": "SE Asia Standard Time", 51 | "KRAT": "North Asia Standard Time", 52 | "IRKT": "North Asia East Standard Time", 53 | "SNST": "Singapore Standard Time", 54 | "AWST": "W. Australia Standard Time", 55 | "TIST": "Taipei Standard Time", 56 | "UST": "Ulaanbaatar Standard Time", 57 | "TST": "Tokyo Standard Time", 58 | "KST": "Korea Standard Time", 59 | "YAKT": "Yakutsk Standard Time", 60 | "CAUST": "Cen. Australia Standard Time", 61 | "ACST": "AUS Central Standard Time", 62 | "EAST": "E. Australia Standard Time", 63 | "AEST": "AUS Eastern Standard Time", 64 | "WPST": "West Pacific Standard Time", 65 | "TAST": "Tasmania Standard Time", 66 | "VLAT": "Vladivostok Standard Time", 67 | "SBT": "Central Pacific Standard Time", 68 | "NZST": "New Zealand Standard Time", 69 | "UTC12": "UTC+12", 70 | "FJT": "Fiji Standard Time", 71 | "PETT": "Kamchatka Standard Time", 72 | "PHOT": "Tonga Standard Time", 73 | "AZOST": "Azores Standard Time", 74 | "CVT": "Cape Verde Standard Time", 75 | "ESAST": "E. South America Standard Time", 76 | "ART": "Argentina Standard Time", 77 | "SAEST": "SA Eastern Standard Time", 78 | "GNST": "Greenland Standard Time", 79 | "MVST": "Montevideo Standard Time", 80 | "NST": "Newfoundland Standard Time", 81 | "PRST": "Paraguay Standard Time", 82 | "CBST": "Central Brazilian Standard Time", 83 | "SAWST": "SA Western Standard Time", 84 | "PSAST": "Pacific SA Standard Time", 85 | "VST": "Venezuela Standard Time", 86 | "SAPST": "SA Pacific Standard Time", 87 | "EST": "US Eastern Standard Time", 88 | "CAST": "Central America Standard Time", 89 | "CST": "Central Standard Time (Mexico)", 90 | "CCST": "Canada Central Standard Time", 91 | "MSTM": "Mountain Standard Time (Mexico)", 92 | "PST": "Pacific Standard Time (Mexico)", 93 | "SMST": "Samoa Standard Time", 94 | "BIT": "Dateline Standard Time", 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Timezones CLI

2 | 3 |

CLI toolkit for timezones:zap:

4 |

5 | 6 | 7 | 8 | 9 |

10 | 11 | 12 | 13 | 14 | ## What can you do with `timezones-cli`? :sparkles: 15 | - Search for date and time based on city, country, or timezones. 16 | - Manage dashboard for timezones you frequently view. 17 | - Get UTC date and time based on your local timezone or any timezones. 18 | 19 | ## Contents 20 | 21 | - [Installation](#installation) 22 | - [Usage](#usage) 23 | - [Search for timezones](#search-for-timezones) 24 | - [Add/save timezones](#addsave-timezones) 25 | - [Remove timezones](#remove-timezones) 26 | - [Show saved timezones](#show-saved-timezones) 27 | - [Select individual timezones from saved](#select-individual-timezones-from-saved) 28 | - [Get UTC time](#get-utc-time) 29 | - [Run using Docker :whale:](#run-using-docker-whale) 30 | - [Contributing](#contributing) 31 | 32 | 33 | ## Installation 34 | 35 | ```bash 36 | $ pip3 install timezones-cli 37 | ``` 38 | To run this CLI using Docker, check [Run using Docker :whale:](#run-using-docker-whale). 39 | 40 | > **NOTE:** [List of country codes or timezone names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) :earth_asia: 41 | 42 | > **Use `-t` flag to toggle 24 hours format.** 43 | 44 | ## Usage 45 | 46 | ### Search for timezones 47 | 48 | Get time based on the entered timezone or country code 49 | 50 | - using country code (either 2 or 3 letters): 51 | 52 | ```bash 53 | $ tz search US 54 | 55 | $ tz search USA 56 | ``` 57 | 58 | - using timezone: 59 | ```bash 60 | $ tz search Asia/Kathmandu 61 | ``` 62 | 63 | - using fuzzy text: (example: Ireland) 64 | ```bash 65 | $ tz search Irela 66 | ``` 67 | 68 | - using timezone shortcodes (--zone or -z flag): 69 | ```bash 70 | $ tz search pst -z 71 | 72 | $ tz search ist -z 73 | 74 | $ tz search jst -z 75 | 76 | $ tz search cest -z 77 | 78 | $ tz search +0543 -z 79 | 80 | $ tz search +05 -z 81 | ``` 82 | 83 |
Demo 84 | 85 | demo of timezone cli search 86 |
87 | 88 | --- 89 | 90 | ### Add/save timezones 91 | 92 | Timezones added to the config file are treated as the default timezones which is triggered by the `tz show` command. 93 | 94 | > file is stored at ~/.tz-cli 95 | 96 | ```bash 97 | $ tz add "Asia/Kathmandu" 98 | ``` 99 | 100 |
Demo 101 | 102 | demo of timezone cli add 103 |
104 | --- 105 | 106 | ### Remove timezones 107 | 108 | There are two ways for removing timezones from the config file. Using the `--interactive` mode and passing the the `--name` flag. 109 | 110 | ```bash 111 | $ tz remove -i 112 | 113 | $ tz remove --name "Asia/Kathmandu" 114 | ``` 115 | 116 |
Demo 117 | 118 | demo of timezone cli remove 119 |
120 | 121 | --- 122 | 123 | ### Show saved timezones 124 | 125 | ```bash 126 | $ tz show 127 | ``` 128 | 129 |
Demo 130 | 131 | demo of timezone cli show 132 |
133 | --- 134 | 135 | ### Select individual timezones from saved 136 | 137 | ```bash 138 | $ tz select 139 | ``` 140 | 141 |
Demo 142 | 143 | demo of timezone cli select 144 |
145 | 146 | --- 147 | 148 | ### Get UTC time 149 | 150 | Get UTC time based on current system time. 151 | 152 | > **tz utc --help** 153 | 154 | ```bash 155 | $ tz utc 156 | ``` 157 | 158 | Get UTC time based on specified time and timezone. 159 | 160 | ```bash 161 | $ tz utc