Compare commits
15 Commits
master
...
developing
| Author | SHA1 | Date |
|---|---|---|
|
|
dc6528e308 | |
|
|
c35b700e06 | |
|
|
46387123bc | |
|
|
c2b103f598 | |
|
|
74c70bca76 | |
|
|
4b790828a8 | |
|
|
59178d3cec | |
|
|
e8d5159973 | |
|
|
b2e80a8c07 | |
|
|
e90ecc60cb | |
|
|
772020a9e5 | |
|
|
f6a5c15eac | |
|
|
855f367727 | |
|
|
fac9a70548 | |
|
|
7a75126708 |
44
README.md
44
README.md
|
|
@ -1,43 +1 @@
|
||||||
# METAR NAVIGATE
|
# METAR NAVIGATE
|
||||||
A Simple METAR Application that feeds from CheckWX API
|
|
||||||
|
|
||||||
It has two modes of display, a simple CLI using Rich and a TUI using Textual
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
First, you need to install the application by issuing:
|
|
||||||
```bash
|
|
||||||
pip install -e .
|
|
||||||
```
|
|
||||||
this will install the application and the dependencies
|
|
||||||
after that you'll need to configure the application by issuing:
|
|
||||||
```bash
|
|
||||||
metarNavigate --no-tui -c
|
|
||||||
```
|
|
||||||
or
|
|
||||||
```bash
|
|
||||||
metarNavigate --no-tui --configure
|
|
||||||
```
|
|
||||||
|
|
||||||
you'll need a checkWX API key to use the application which is free
|
|
||||||
|
|
||||||
after the configuration process you can use the application by issuing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
metarNavigate --no-tui <ICAO>
|
|
||||||
```
|
|
||||||
this will run the application in CLI mode and display the METAR for the ICAO code issued as argument.
|
|
||||||
|
|
||||||
If you wish to see a detailed breakdown of the METAR in CLI mode, you can issue:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
metarNavigate --no-tui --detailed <ICAO>
|
|
||||||
```
|
|
||||||
or
|
|
||||||
```bash
|
|
||||||
metarNavigate --no-tui -d <ICAO>
|
|
||||||
```
|
|
||||||
this will print all details of the METAR Message with an explanation of each part of the message.
|
|
||||||
|
|
||||||
>[!NOTE]
|
|
||||||
>
|
|
||||||
>The flags ``` -d ``` or the flag ```--detailed ``` can you be used with the flag ```--no-tui ```
|
|
||||||
|
|
@ -17,6 +17,11 @@ classifiers = [
|
||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"tomli",
|
||||||
|
"tomli_w",
|
||||||
|
"rich",
|
||||||
|
"requests"
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from metar_navigate.application import Application
|
||||||
|
from metar_navigate.command import main
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Application", "main"]
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from metar_navigate.services import METARService
|
||||||
|
from metar_navigate.ui import RichInterface
|
||||||
|
from metar_navigate.utils import ConfigManager
|
||||||
|
|
||||||
|
class Application:
|
||||||
|
def __init__(self):
|
||||||
|
self.parser = None
|
||||||
|
self.args = None
|
||||||
|
self.config = ConfigManager()
|
||||||
|
self.METARService = METARService(self)
|
||||||
|
self.ui = None
|
||||||
|
self.is_configured = False
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
|
||||||
|
if self.config.read_config() is not None:
|
||||||
|
self.is_configured = True
|
||||||
|
|
||||||
|
self._validate_args()
|
||||||
|
|
||||||
|
if self.args.no_tui:
|
||||||
|
self.ui = RichInterface(self)
|
||||||
|
if self.args.configure:
|
||||||
|
self.ui.setup(configure=True)
|
||||||
|
if self.args.detailed:
|
||||||
|
self.ui.setup(detailed=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_args(self):
|
||||||
|
self.parser = argparse.ArgumentParser()
|
||||||
|
self.parser.add_argument("--no-tui", action="store_true", help="disables the TUI")
|
||||||
|
self.parser.add_argument("--configure", "-c" ,action="store_true", help="runs the configuration wizard")
|
||||||
|
self.parser.add_argument("--detailed", action="store_true", help="displays more detailed METAR Breakdown "
|
||||||
|
"(Only available with --no-tui flag)")
|
||||||
|
self.parser.add_argument("-t", "--type", choices=["TAF", "METAR"], default="METAR", help="Select the Type "
|
||||||
|
"of message (Only Available with"
|
||||||
|
" --no-tui flag)")
|
||||||
|
self.parser.add_argument("ICAO", nargs='?', type=str.upper,help="ICAO code of airport (required with --no-tui flag)")
|
||||||
|
|
||||||
|
self.args = self.parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.configure and self.args.ICAO:
|
||||||
|
self.parser.error("--configure não aceita código ICAO")
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.configure and self.args.detailed:
|
||||||
|
self.parser.error("--configure não pode ser usado com --detailed")
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.configure and ('--type' in sys.argv or '-t' in sys.argv):
|
||||||
|
self.parser.error("--configure não pode ser usado com --type/-t")
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.no_tui and not self.args.configure and not self.args.ICAO:
|
||||||
|
self.parser.error("--no-tui requer código ICAO (exceto quando usado com --configure)")
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.ICAO and not self.args.no_tui:
|
||||||
|
self.parser.error("Código ICAO só pode ser usado com --no-tui")
|
||||||
|
|
||||||
|
|
||||||
|
if self.args.detailed and not self.args.no_tui:
|
||||||
|
self.parser.error("--detailed requer --no-tui")
|
||||||
|
|
||||||
|
|
||||||
|
if not self.args.no_tui and ('--type' in sys.argv or '-t' in sys.argv):
|
||||||
|
self.parser.error("--type/-t requer --no-tui")
|
||||||
|
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.ui.run()
|
||||||
|
|
@ -1,2 +1,6 @@
|
||||||
|
from metar_navigate import Application
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("Hello World")
|
app = Application()
|
||||||
|
app.setup()
|
||||||
|
app.run()
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
class METARService:
|
||||||
|
def __init__(self,parent):
|
||||||
|
self.__parent = parent
|
||||||
|
self.url = "https://api.checkwx.com/metar/"
|
||||||
|
def request_metar(self,ICAO_code):
|
||||||
|
url = self.url + ICAO_code
|
||||||
|
header = {
|
||||||
|
'X-API-Key':self.__parent.config.get_data("checkwx_api_key").get("checkwx_api_key")
|
||||||
|
}
|
||||||
|
if self.__parent.args.detailed:
|
||||||
|
url = url + "/decoded"
|
||||||
|
response = requests.request('GET', url,headers=header)
|
||||||
|
return response
|
||||||
|
def get_metar(self,ICAO_code):
|
||||||
|
request = self.request_metar(ICAO_code)
|
||||||
|
|
||||||
|
return request
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .METARSerice import METARService
|
||||||
|
|
||||||
|
__all__ = ["METARService"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .ui import RichInterface
|
||||||
|
|
||||||
|
__all__ = ["RichInterface",]
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from rich import console
|
||||||
|
|
||||||
|
|
||||||
|
class UIInterface(ABC):
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def setup(self):
|
||||||
|
pass
|
||||||
|
@abstractmethod
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RichInterface(UIInterface):
|
||||||
|
def __init__(self,parent):
|
||||||
|
super().__init__()
|
||||||
|
self.__parent = parent
|
||||||
|
self.console = console.Console()
|
||||||
|
self.console.show_cursor(False)
|
||||||
|
self.mode = None
|
||||||
|
|
||||||
|
def setup(self,**kwargs):
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
match(key):
|
||||||
|
case "configure":
|
||||||
|
if value:
|
||||||
|
self.mode = "configure"
|
||||||
|
else:
|
||||||
|
self.mode = "run"
|
||||||
|
case "detailed":
|
||||||
|
self.mode = "detailed"
|
||||||
|
case _:
|
||||||
|
raise ValueError(f"Invalid mode: {key}")
|
||||||
|
def configure_run_mode(self):
|
||||||
|
self.console.print("This Application is in [bold red]Configure Mode[/bold red]")
|
||||||
|
self.console.print("This Application requires a CheckWX API Key to run")
|
||||||
|
print("Please enter your CheckWX API:",end='')
|
||||||
|
api_key = input()
|
||||||
|
print(f"\033[A\rPlease enter your CheckWX API:{' ' * len(api_key)}\rPlease enter your CheckWX API:",end='')
|
||||||
|
while api_key == '':
|
||||||
|
self.console.print("[bold red]Invalid API Key: Can't be empty [/bold red]")
|
||||||
|
print("Please enter your CheckWX API:", end='')
|
||||||
|
api_key = input()
|
||||||
|
print(f"\033[A\rPlease enter your CheckWX API:{' ' * len(api_key)}\rPlease enter your CheckWX API:", end='')
|
||||||
|
print("\n")
|
||||||
|
always_show_detailed = self.console.input("Always Show Detailed (default is: [green]No[/green]) [[u]y[/u]/[u]N[/u]]: ").upper()
|
||||||
|
if always_show_detailed == '':
|
||||||
|
always_show_detailed = 'N'
|
||||||
|
while always_show_detailed not in ['Y', 'N']:
|
||||||
|
always_show_detailed = self.console.input("Always Show Detailed (default is: [green]No[/green]) [[u]y[/u]/[u]N[/u]]: ").upper()
|
||||||
|
if always_show_detailed == '':
|
||||||
|
always_show_detailed = 'N'
|
||||||
|
|
||||||
|
wind_speed_unit = self.console.input(
|
||||||
|
'Speed Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]M[/u]iles/K[u]N[/u]ots/KM/[u]H[/u]]: '
|
||||||
|
).upper()
|
||||||
|
|
||||||
|
if wind_speed_unit == '':
|
||||||
|
wind_speed_unit = 'K'
|
||||||
|
while wind_speed_unit not in ['K', 'M', 'N', 'H']:
|
||||||
|
wind_speed_unit = self.console.input(
|
||||||
|
'Speed Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]M[/u]iles/K[u]N[/u]ots/KM/[u]H[/u]]: '
|
||||||
|
).upper()
|
||||||
|
if wind_speed_unit == '':
|
||||||
|
wind_speed_unit = 'K'
|
||||||
|
|
||||||
|
temp_unit = self.console.input(
|
||||||
|
'Temperature Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]C[/u]elsius/[u]F[/u]ahrenheit]: '
|
||||||
|
).upper()
|
||||||
|
if temp_unit == '':
|
||||||
|
temp_unit = 'K'
|
||||||
|
while temp_unit not in ['K', 'C', 'F']:
|
||||||
|
temp_unit = self.console.input(
|
||||||
|
'Temperature Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]C[/u]elsius/[u]F[/u]ahrenheit]: '
|
||||||
|
).upper()
|
||||||
|
if temp_unit == '':
|
||||||
|
temp_unit = 'K'
|
||||||
|
|
||||||
|
visibility_unit = self.console.input(
|
||||||
|
'Visibility Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]S[/u]M/[u]M[/u]]: '
|
||||||
|
).upper()
|
||||||
|
if visibility_unit == '':
|
||||||
|
visibility_unit = 'K'
|
||||||
|
while visibility_unit not in ['K', 'S', 'M']:
|
||||||
|
visibility_unit = self.console.input(
|
||||||
|
'Visibility Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]S[/u]M/[u]M[/u]]: '
|
||||||
|
).upper()
|
||||||
|
if visibility_unit == '':
|
||||||
|
visibility_unit = 'K'
|
||||||
|
|
||||||
|
pressure_unit = self.console.input(
|
||||||
|
'Pressure Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]I[/u]nHg/[u]H[/u]Pa]: '
|
||||||
|
).upper()
|
||||||
|
if pressure_unit == '':
|
||||||
|
pressure_unit = 'K'
|
||||||
|
while pressure_unit not in ['K', 'H', 'U']:
|
||||||
|
pressure_unit = self.console.input(
|
||||||
|
'Pressure Unit (default is: [green]K[/green]) [[u]K[/u]eep/[u]I[/u]nHg/[u]H[/u]Pa]: '
|
||||||
|
).upper()
|
||||||
|
if pressure_unit == '':
|
||||||
|
pressure_unit = 'K'
|
||||||
|
|
||||||
|
self.__parent.config.set_data_dict({ "checkwx_api_key": api_key})
|
||||||
|
|
||||||
|
if always_show_detailed == 'Y':
|
||||||
|
self.__parent.config.set_data_dict({"always_show_detailed":True})
|
||||||
|
else:
|
||||||
|
self.__parent.config.set_data_dict({"always_show_detailed": False})
|
||||||
|
|
||||||
|
if wind_speed_unit == 'K':
|
||||||
|
self.__parent.config.set_data_dict({"wind_speed_unit":"Keep"})
|
||||||
|
elif wind_speed_unit == 'M':
|
||||||
|
self.__parent.config.set_data_dict({"wind_speed_unit":"Miles"})
|
||||||
|
elif wind_speed_unit == 'N':
|
||||||
|
self.__parent.config.set_data_dict({"wind_speed_unit": "Knots"})
|
||||||
|
elif wind_speed_unit == 'H':
|
||||||
|
self.__parent.config.set_data_dict({"wind_speed_unit": "KM/H"})
|
||||||
|
|
||||||
|
if temp_unit == 'K':
|
||||||
|
self.__parent.config.set_data_dict({"temp_unit":"Keep"})
|
||||||
|
elif temp_unit == 'C':
|
||||||
|
self.__parent.config.set_data_dict({ "temp_unit": "Celsius"})
|
||||||
|
elif temp_unit == 'F':
|
||||||
|
self.__parent.config.set_data_dict({ "temp_unit": "Fahrenheit"})
|
||||||
|
|
||||||
|
if visibility_unit == 'K':
|
||||||
|
self.__parent.config.set_data_dict({ "visibility_unit": "Keep"})
|
||||||
|
elif visibility_unit == 'S':
|
||||||
|
self.__parent.config.set_data_dict({"visibility_unit": "SM"})
|
||||||
|
elif visibility_unit == 'M':
|
||||||
|
self.__parent.config.set_data_dict({"visibility_unit": "Miles"})
|
||||||
|
|
||||||
|
if pressure_unit == 'K':
|
||||||
|
self.__parent.config.set_data_dict({"pressure_unit": "Keep"})
|
||||||
|
elif pressure_unit == 'H':
|
||||||
|
self.__parent.config.set_data_dict({ "pressure_unit": "HPa"})
|
||||||
|
elif pressure_unit == 'I':
|
||||||
|
self.__parent.config.set_data_dict({ "pressure_unit": "InHg"})
|
||||||
|
|
||||||
|
self.__parent.config.write_config()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.__parent.is_configured and self.mode != "configure":
|
||||||
|
self.console.print("[bold red]This Application Requires a configuration to run please run with -c flag to configure [/bold red]")
|
||||||
|
self.console.show_cursor(True)
|
||||||
|
exit(69)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if self.mode == "configure":
|
||||||
|
self.configure_run_mode()
|
||||||
|
else:
|
||||||
|
self.console.print("This Application is in [bold red]Run Mode[/bold red]")
|
||||||
|
|
||||||
|
self.console.show_cursor(True)
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .directory_manager import DirectoryManager
|
||||||
|
from .config_manager import ConfigManager
|
||||||
|
|
||||||
|
__all__ = ["DirectoryManager", "ConfigManager"]
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import tomli
|
||||||
|
import tomli_w
|
||||||
|
|
||||||
|
from metar_navigate.utils import DirectoryManager
|
||||||
|
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
class SingletonMeta(type):
|
||||||
|
_instances = {}
|
||||||
|
_lock: Lock = Lock()
|
||||||
|
|
||||||
|
def __call__(cls, *args, **kwargs):
|
||||||
|
with cls._lock:
|
||||||
|
if cls not in cls._instances:
|
||||||
|
instance = super().__call__(*args, **kwargs)
|
||||||
|
cls._instances[cls] = instance
|
||||||
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager(metaclass=SingletonMeta):
|
||||||
|
def __init__(self):
|
||||||
|
self.config_dir = DirectoryManager.get_config_directory()
|
||||||
|
self.__data = dict()
|
||||||
|
|
||||||
|
def read_config(self):
|
||||||
|
if os.path.exists(f"{self.config_dir}/config.toml"):
|
||||||
|
try:
|
||||||
|
with open(f"{self.config_dir}/config.toml", "rb") as f:
|
||||||
|
data = tomli.load(f)
|
||||||
|
except tomli.TOMLDecodeError as e:
|
||||||
|
raise ValueError(f"Invalid TOML file; {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Error reading config file; {e}")
|
||||||
|
|
||||||
|
self.__data = data
|
||||||
|
return self.__data
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
def set_data_dict(self,data):
|
||||||
|
self.__data.update(data)
|
||||||
|
|
||||||
|
|
||||||
|
def write_config(self):
|
||||||
|
default = {
|
||||||
|
"checkwx_api_key": "",
|
||||||
|
"always_show_detailed": False,
|
||||||
|
"wind_speed_unit": "Keep",
|
||||||
|
"temp_unit": "Keep",
|
||||||
|
"visibility_unit": "Keep",
|
||||||
|
"pressure_unit": "Keep"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, default_value in default.items():
|
||||||
|
current_value = self.__data.get(key)
|
||||||
|
if current_value is None or current_value == "":
|
||||||
|
self.__data[key] = default_value
|
||||||
|
else:
|
||||||
|
self.__data[key] = current_value
|
||||||
|
with open(f"{self.config_dir}/config.toml", "wb") as f:
|
||||||
|
tomli_w.dump(self.__data, f)
|
||||||
|
|
||||||
|
def get_data(self,key):
|
||||||
|
if key in self.__data.keys():
|
||||||
|
return {key:self.__data[key]}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryManager:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_config_directory()-> Path:
|
||||||
|
home = Path.home()
|
||||||
|
config_dir = home / ".METARNavigate"
|
||||||
|
config_dir.mkdir(exist_ok=True)
|
||||||
|
os.chmod(config_dir,0o744)
|
||||||
|
|
||||||
|
return config_dir
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue