Compare commits

...

15 Commits

Author SHA1 Message Date
Gustavo Henrique Santos Souza de Miranda dc6528e308 Add `METARService` for retrieving METAR data and integrate it with `Application`
- Implement `METARService` to fetch METAR data from the CheckWX API.
- Add `requests` dependency to `pyproject.toml`.
- Update `Application` to initialize and use `METARService`.
2025-10-31 17:59:27 -03:00
Gustavo Henrique Santos Souza de Miranda c35b700e06 Fix `get_data` method to correctly check keys in `ConfigManager` data structure 2025-10-31 17:52:10 -03:00
Gustavo Henrique Santos Souza de Miranda 46387123bc Add `get_data` method to `ConfigManager` for retrieving specific configuration keys 2025-10-31 17:39:42 -03:00
Gustavo Henrique Santos Souza de Miranda c2b103f598 Refactor UI configuration logic to use `set_data_dict` and add config persistence with `write_config` 2025-10-31 00:49:38 -03:00
Gustavo Henrique Santos Souza de Miranda 74c70bca76 Update directory permissions in `DirectoryManager` to allow execution 2025-10-31 00:49:22 -03:00
Gustavo Henrique Santos Souza de Miranda 4b790828a8 Initialize `__data` in `ConfigManager` as an empty dictionary 2025-10-31 00:44:00 -03:00
Gustavo Henrique Santos Souza de Miranda 59178d3cec Add default handling and write functionality to `ConfigManager`
- Ensure missing or empty config values are replaced with defaults.
- Write updated configuration back to `config.toml` using `tomli_w`.
2025-10-31 00:27:33 -03:00
Gustavo Henrique Santos Souza de Miranda e8d5159973 Add write capability to `ConfigManager` using `tomli_w`
- Introduced `set_data_dict` method to update configuration data.
- Added `write_config` method to generate default config structure for writing.
2025-10-30 18:09:44 -03:00
Gustavo Henrique Santos Souza de Miranda b2e80a8c07 Remove unused imports and clean up UI code 2025-10-30 17:47:31 -03:00
Gustavo Henrique Santos Souza de Miranda e90ecc60cb Add configuration prompts and validation to `RichInterface`
- Enhanced `RichInterface` with interactive prompts for API key and user preferences (e.g., speed, temperature, visibility, and pressure units).
- Added input validation to ensure correct configuration values.
- Integrated parent `Application` reference for seamless configuration updates.
- Improved error handling to enforce configuration setup before application execution.
2025-10-30 16:54:29 -03:00
Gustavo Henrique Santos Souza de Miranda 772020a9e5 Refactor `Application` and `RichInterface` setup logic
- Added `is_configured` flag in `Application` to track configuration state.
- Updated `RichInterface` to support multiple modes (`configure`, `detailed`, etc.) using structured logic.
- Integrated TUI run modes and refined display messages.
- Fixed minor imports and style inconsistencies in `RichInterface`.
2025-10-30 01:28:36 -03:00
Gustavo Henrique Santos Souza de Miranda f6a5c15eac Add TUI integration and refactor setup logic
- Introduced `RichInterface` to support Textual User Interface (TUI) functionality.
- Enhanced `Application` class to initialize and configure TUI based on arguments.
- Added `ui` module with a base `UIInterface` and `RichInterface` implementation.
- Updated argument parsing logic to handle TUI-specific options.
2025-10-30 00:32:21 -03:00
Gustavo Henrique Santos Souza de Miranda 855f367727 Add configuration management and improve argument validation logic
- Introduced `DirectoryManager` to handle configuration directory creation.
- Added `ConfigManager` with singleton design to manage config reading.
- Expanded argument validation in `Application` to refine error checks.
- Updated dependencies to include `rich`, `tomli`, and `tomli_w`.
- Adjusted setup logic to prompt users about missing configurations.
2025-10-29 19:49:20 -03:00
Gustavo Henrique Santos Souza de Miranda fac9a70548 Add argument parsing to `Application` class and integrate setup in entry point 2025-10-29 03:04:09 -03:00
Gustavo Henrique Santos Souza de Miranda 7a75126708 Introduce `Application` class and reorganize module structure 2025-10-29 00:52:27 -03:00
11 changed files with 369 additions and 1 deletions

View File

@ -17,6 +17,11 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
"tomli",
"tomli_w",
"rich",
"requests"
] ]

View File

@ -0,0 +1,5 @@
from metar_navigate.application import Application
from metar_navigate.command import main
__all__ = ["Application", "main"]

View File

@ -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()

View File

@ -1,2 +1,6 @@
from metar_navigate import Application
def main(): def main():
print("Hello World") app = Application()
app.setup()
app.run()

View File

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

View File

@ -0,0 +1,3 @@
from .METARSerice import METARService
__all__ = ["METARService"]

View File

@ -0,0 +1,3 @@
from .ui import RichInterface
__all__ = ["RichInterface",]

157
src/metar_navigate/ui/ui.py Normal file
View File

@ -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)

View File

@ -0,0 +1,4 @@
from .directory_manager import DirectoryManager
from .config_manager import ConfigManager
__all__ = ["DirectoryManager", "ConfigManager"]

View File

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

View File

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