diff --git a/README.md b/README.md index a0d81e6..fd93173 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Skylab is a text user interface (TUI) tool that displays upcoming space launches in a user-friendly way. -![skylab](https://i.imgur.com/96seWOP.png) +![skylab](https://i.imgur.com/Hopa3mN.png) Skylab is built using the [Textual](https://github.com/Textualize/textual) framework. diff --git a/poetry.lock b/poetry.lock index cd6dc49..331f005 100644 --- a/poetry.lock +++ b/poetry.lock @@ -459,21 +459,21 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.4.1" +version = "4.13.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_metadata-6.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"}, - {file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"}, + {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, + {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] @@ -491,14 +491,14 @@ files = [ [[package]] name = "linkify-it-py" -version = "2.0.0" +version = "2.0.2" description = "Links recognition library with FULL unicode support." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "linkify-it-py-2.0.0.tar.gz", hash = "sha256:476464480906bed8b2fa3813bf55566282e55214ad7e41b7d1c2b564666caf2f"}, - {file = "linkify_it_py-2.0.0-py3-none-any.whl", hash = "sha256:1bff43823e24e507a099e328fc54696124423dd6320c75a9da45b4b754b748ad"}, + {file = "linkify-it-py-2.0.2.tar.gz", hash = "sha256:19f3060727842c254c808e99d465c80c49d2c7306788140987a1a7a29b0d6ad2"}, + {file = "linkify_it_py-2.0.2-py3-none-any.whl", hash = "sha256:a3a24428f6c96f27370d7fe61d2ac0be09017be5190d68d8658233171f1b6541"}, ] [package.dependencies] @@ -506,7 +506,7 @@ uc-micro-py = "*" [package.extras] benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit"] +dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] doc = ["myst-parser", "sphinx", "sphinx-book-theme"] test = ["coverage", "pytest", "pytest-cov"] @@ -1001,14 +1001,14 @@ devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pyte [[package]] name = "uc-micro-py" -version = "1.0.1" +version = "1.0.2" description = "Micro subset of unicode data files for linkify-it-py projects." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "uc-micro-py-1.0.1.tar.gz", hash = "sha256:b7cdf4ea79433043ddfe2c82210208f26f7962c0cfbe3bacb05ee879a7fdb596"}, - {file = "uc_micro_py-1.0.1-py3-none-any.whl", hash = "sha256:316cfb8b6862a0f1d03540f0ae6e7b033ff1fa0ddbe60c12cbe0d4cec846a69f"}, + {file = "uc-micro-py-1.0.2.tar.gz", hash = "sha256:30ae2ac9c49f39ac6dce743bd187fcd2b574b16ca095fa74cd9396795c954c54"}, + {file = "uc_micro_py-1.0.2-py3-none-any.whl", hash = "sha256:8c9110c309db9d9e87302e2f4ad2c3152770930d88ab385cd544e7a7e75f3de0"}, ] [package.extras] @@ -1138,4 +1138,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "95ccd29962300e4fd29d8432138d8d03ca3691aec0c92d4425217ea9361ffc16" +content-hash = "2deea326a721149fd0f7383c0d33e402d3ec0e844addf46978c27649db2b5ace" diff --git a/pyproject.toml b/pyproject.toml index 3f8f1a3..16d8ae0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "skylab" -version = "0.1.2" +version = "0.2.1" description = "A TUI for showing latest upcoming rocket launches." authors = ["SerhiiStets "] license = "MIT" @@ -10,7 +10,7 @@ repository="https://github.com/SerhiiStets/skylab" [tool.poetry.dependencies] python = "^3.9" pytest = "^7.3.1" -textual = {extras = ["dev"], version = "^0.19.1"} +textual = {version = "^0.19.1", extras = ["dev"]} pydantic = "^1.10.7" tzlocal = "^4.3" requests = "^2.28.2" diff --git a/skylab/api.py b/skylab/api.py index 223a740..0f8e7ac 100644 --- a/skylab/api.py +++ b/skylab/api.py @@ -1,39 +1,58 @@ """Api module for receiving data from thespacedevs.""" + import datetime import requests -from skylab.models import Launch - -GET_UPCOMING_LAUNCHES = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/" - - -def get_upcoming_laucnhes(url: str, timeout: float = 5.0) -> dict: - """Get request for upcoming launches.""" - try: - response = requests.get(url, timeout=timeout) - response.raise_for_status() - return response.json() - except requests.exceptions.RequestException as ex: - raise ValueError(f"Failed to retrieve upcoming launches: {ex}.") - - -def launch_factory() -> list[Launch]: - """Makes a request for upcoming launches, modifies and validates them.""" - results = get_upcoming_laucnhes(GET_UPCOMING_LAUNCHES)["results"] - launches = [] - for launch_dict in results: - # checking if the launch date is not expired - net = datetime.datetime.fromisoformat(launch_dict["net"][:-1]) - if net < datetime.datetime.now(): - continue - - # removing "configuration" nested layer from given data - rocket_config = launch_dict["rocket"].pop("configuration") - launch_dict["rocket"].update(rocket_config) - - # getting address name from "location" json - launch_pad_address = launch_dict["pad"].pop("location")["name"] - launch_dict["pad"]["address"] = launch_pad_address - launches.append(Launch(**launch_dict)) - return launches +from skylab.models.event_models import Event +from skylab.models.launch_models import Launch + + +class SpaceDevApi: + """Interface for interacting with SpaceDev API.""" + + BASE_URL = "https://ll.thespacedevs.com/2.2.0" + GET_UPCOMING_LAUNCHES = f"{BASE_URL}/launch/upcoming/" + GET_UPCOMING_EVENTS = f"{BASE_URL}/event/upcoming/" + + def __init__(self, timeout: float = 5.0) -> None: + """Initialize SpaceDevApi object.""" + self.timeout = timeout + self.session = requests.Session() + + def get_request(self, url: str) -> dict: + """Make a get request to the SpaceDev API.""" + try: + response = self.session.get(url, timeout=self.timeout) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as ex: + raise ValueError(f"Failed to retrieve the info: {ex}.") + + def launch_factory(self) -> list[Launch]: + """Makes a request for upcoming launches, modifies and validates them.""" + results = self.get_request(self.GET_UPCOMING_LAUNCHES)["results"] + launches = [] + for launch_dict in results: + # checking if the launch date is not expired + net = datetime.datetime.fromisoformat(launch_dict["net"][:-1]) + if net < datetime.datetime.now(): + continue + + # removing "configuration" nested layer from given data + rocket_config = launch_dict["rocket"].pop("configuration") + launch_dict["rocket"].update(rocket_config) + + # getting address name from "location" json + launch_pad_address = launch_dict["pad"].pop("location")["name"] + launch_dict["pad"]["address"] = launch_pad_address + launches.append(Launch(**launch_dict)) + return launches + + def event_factory(self) -> list[Event]: + """Make a request for upcoming events, modifies and validates them.""" + results = self.get_request(self.GET_UPCOMING_EVENTS)["results"] + events = [] + for event_dict in results: + events.append(Event(**event_dict)) + return events diff --git a/skylab/css/styles.css b/skylab/css/styles.css index 429b317..7f8c73f 100644 --- a/skylab/css/styles.css +++ b/skylab/css/styles.css @@ -1,9 +1,37 @@ +.bg-text{ + text-opacity: 70%; +} + + +.title{ + text-style: underline; +} + +#title-grid{ + layout: grid; + grid-size: 2 1; + height: 1; + margin-left: 3; + margin-top: 1; + margin-bottom: 1; + grid-columns: 2fr 1fr; + grid-gutter:3; +} + +#window-split { + layout: grid; + grid-size: 2 1; + grid-columns: 2fr 1fr; +} + LaunchWidget { layout: grid; grid-size: 2 1; grid-gutter:2; - grid-columns: 2fr 1fr; - margin: 3; + grid-columns: 4fr 1fr; + margin-left: 2; + margin-right: 2; + margin-bottom: 2; padding-left: 5; padding-right: 5; padding-top: 2; @@ -11,18 +39,36 @@ LaunchWidget { height: 16; } -TimeDisplay { + +#right-launch-widget{ + width: 20; + grid-gutter: 1 4; content-align: center middle; - text-opacity: 60%; - height: 3; } -#watch_button{ - dock:right; - height: 3; +#launch-counter{ content-align: center middle; + height: 3; +} + +#watch-button{ + align: center middle; + width: 100%; } -.launch-time { - text-style: bold; +/* Event widgets */ + +EventWidget{ + padding: 1; + margin-left: 2; + margin-right: 2; + margin-bottom: 2; +} + +.event-buttons{ + layout: grid; + grid-size: 2 1; + grid-columns: 1fr 1fr; + grid-gutter:2; + height: 3; } diff --git a/skylab/models/__init__.py b/skylab/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skylab/models/event_models.py b/skylab/models/event_models.py new file mode 100644 index 0000000..cd2b9bc --- /dev/null +++ b/skylab/models/event_models.py @@ -0,0 +1,15 @@ +"""Event pydantic models.""" + +from typing import Union + +from pydantic import BaseModel + + +class Event(BaseModel): + """Event model from SpaceDev API.""" + + name: str + description: str = "No description yet." + date: Union[str, None] + news_url: Union[str, None] + video_url: Union[str, None] diff --git a/skylab/models.py b/skylab/models/launch_models.py similarity index 86% rename from skylab/models.py rename to skylab/models/launch_models.py index 100566a..71e0b06 100644 --- a/skylab/models.py +++ b/skylab/models/launch_models.py @@ -6,24 +6,34 @@ class LaunchProvier(BaseModel): + """Provider information for the launch (i.e. SpaceX).""" + name: str class Mission(BaseModel): + """Mission information for the launch.""" + name: str description: str class Rocket(BaseModel): + """Rocket configuration for the launch.""" + full_name: str class LaunchPad(BaseModel): + """Launch pad information for the launch.""" + name: str address: str class Launch(BaseModel): + """Launch model from SpaceDev API.""" + name: str net: str pad: LaunchPad = Field(LaunchPad(name="Unknown", address="Unknown")) diff --git a/skylab/settings.py b/skylab/settings.py new file mode 100644 index 0000000..295c6ff --- /dev/null +++ b/skylab/settings.py @@ -0,0 +1,9 @@ +"""Skylab const and settings module.""" + +import os + +import tzlocal + +CURR_DIR = os.path.dirname(os.path.abspath(__file__)) +LOCAL_TIMEZONE = tzlocal.get_localzone() +CSS_PATH = os.path.join(CURR_DIR, "css/styles.css") diff --git a/skylab/skylab.py b/skylab/skylab.py index fe5baaf..12a43b4 100644 --- a/skylab/skylab.py +++ b/skylab/skylab.py @@ -1,145 +1,68 @@ """Skylab TUI.""" -import datetime -import os import random -import urllib.parse -import webbrowser -import tzlocal from textual.app import App, ComposeResult from textual.color import Color from textual.containers import Container -from textual.reactive import reactive -from textual.widgets import Button, Footer, Header, Static +from textual.widgets import Footer, Header, Static -import skylab.api -from skylab.models import Launch - -CURR_DIR = os.path.dirname(os.path.abspath(__file__)) -LOCAL_TIMEZONE = tzlocal.get_localzone() - - -class TimeDisplay(Static): - """Time till launch Static.""" - - time_to_launch: reactive[datetime.timedelta] = reactive(datetime.timedelta(0)) - - def __init__(self, *args, **kwargs) -> None: - """Initialize TimeDisplay.""" - # Remove passed time_to_launch var before calling super() - self._launch_time = kwargs.pop("launch_time") - offset = self._launch_time.astimezone(LOCAL_TIMEZONE).utcoffset() - self._offset = offset if offset else datetime.timedelta(0) - super().__init__(*args, **kwargs) - - def on_mount(self) -> None: - """On widget mount sets interval to call update_tim every second.""" - self.time_to_launch = self._launch_time - datetime.datetime.now() + self._offset - self.set_interval(1, self.update_time) - - def update_time(self) -> None: - """Updates time_to_launch value.""" - self.time_to_launch = self._launch_time - datetime.datetime.now() + self._offset - - def watch_time_to_launch(self, time_to_launch: datetime.timedelta) -> None: - """ - Updates screen value of time_to_launch every time the value changes - and removes milliseconds from time_to_launch value to display. - """ - formated_time = datetime.timedelta( - days=time_to_launch.days, seconds=time_to_launch.seconds - ) - self.update(f"{formated_time}") - - -class LaunchWidget(Static): - """Launch object Static.""" - - youtube_url = "https://www.youtube.com" - - def __init__(self, *args, **kwargs) -> None: - """Initialize LaunchWidget.""" - # remove launch object from kwargs before doing super() - self.launch: Launch = kwargs.pop("launch") - super().__init__(*args, **kwargs) - self._net = datetime.datetime.fromisoformat(self.launch.net[:-1]) - self._launch_time = Static( - f"{LOCAL_TIMEZONE.localize(self._net)}\n", classes="launch-time" - ) - self._launch_provider = Static(f"{self.launch.launch_service_provider.name}\n") - self._rocket_name = Static(f"{self.launch.rocket.full_name}\n") - self._launch_pad = Static( - f"{self.launch.pad.name}, {self.launch.pad.address}\n" - ) - self._description = Static(f"{self.launch.mission.description}") - - def on_button_pressed(self) -> None: - """ - Event handler called when a button is pressed. - On press creates a youtube search query for upcoming launch. - """ - query = f"{self.launch.name} {self._net}" - launch_url = ( - f"{self.youtube_url}/results?search_query={urllib.parse.quote(query)}" - ) - webbrowser.open(launch_url, new=2) - - def compose(self) -> ComposeResult: - """Creates a child widget of LaunchWidget.""" - yield Container( - self._launch_time, - self._launch_provider, - self._rocket_name, - self._launch_pad, - self._description, - ) - yield Container( - TimeDisplay(launch_time=self._net, id="time-left"), - Button("Watch", id="watch_button"), - ) +from skylab import settings +from skylab.api import SpaceDevApi +from skylab.widgets.event_widget import EventWidget +from skylab.widgets.launch_widget import LaunchWidget class SkylabApp(App): """Skylab TUI app.""" - TITLE = "skylab" + TITLE = "Skylab" BINDINGS = [ ("d", "exit", "Exit skylab."), ("?", "", "SpaceDev API is free but allows no more than 15 requests per hour"), ] - CSS_PATH = os.path.join(CURR_DIR, "css/styles.css") + CSS_PATH = settings.CSS_PATH def __init__(self, *args, **kwargs): """Initialize SkylabApp.""" super().__init__(*args, **kwargs) - self.launch_widget_list = [ - LaunchWidget(launch=launch) for launch in skylab.api.launch_factory() + api_client = SpaceDevApi() + self._launch_widget_list = [ + LaunchWidget(launch=launch) for launch in api_client.launch_factory() + ] + self._event_widget_list = [ + EventWidget(event=event) for event in api_client.event_factory() ] + self._launch_widgets = Container(*self._launch_widget_list) + self._event_widgets = Container(*self._event_widget_list) + + def action_exit(self) -> None: + """ + Bindings action method for app exit. + As set in Bindings - pressing D would exit the app. + """ + self.exit() def on_mount(self) -> None: """ Event handler for when widget added to the app. - Changes style of given LaunchWidgets such as border, - background-color, border_title, etc. + Changes style of given LaunchWidgets such as background. """ - for launch_widget in self.launch_widget_list: + for launch_widget in self._launch_widget_list: launch_widget.styles.background = Color( random.randint(0, 255), random.randint(0, 255), random.randint(0, 255), 0.45, ) - launch_widget.styles.border = ("round", "white") - launch_widget.border_title = launch_widget.launch.name - launch_widget.styles.border_title_align = "left" - - def action_exit(self) -> None: - """Bindings action method for app exit.""" - self.exit() def compose(self) -> ComposeResult: """Creates a component of SkylabApp.""" yield Header() - yield Container(*self.launch_widget_list) + yield Container( + Static("Launches", classes="title"), + Static("Events", classes="title"), + id="title-grid", + ) + yield Container(self._launch_widgets, self._event_widgets, id="window-split") yield Footer() diff --git a/skylab/widgets/__init__.py b/skylab/widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/skylab/widgets/event_widget.py b/skylab/widgets/event_widget.py new file mode 100644 index 0000000..b770486 --- /dev/null +++ b/skylab/widgets/event_widget.py @@ -0,0 +1,58 @@ +"""Event widget of Skylab app.""" + +import datetime +import webbrowser + +from textual.app import ComposeResult +from textual.containers import Horizontal +from textual.widgets import Button, Static + +from skylab.models.event_models import Event + + +class EventWidget(Static): + """Event object Static.""" + + def __init__(self, *args, **kwargs) -> None: + """Initialize Event widget.""" + self.event: Event = kwargs.pop("event") + super().__init__(*args, **kwargs) + self._explore_disabled = False if self.event.news_url else True + self._watch_disabled = False if self.event.video_url else True + if self.event.date: + self._date = str(datetime.datetime.fromisoformat(self.event.date[:-1])) + else: + self._date = "No date given yet." + + def on_button_pressed(self, event: Button.Pressed) -> None: + """ + Event handler called when a button is pressed. + On press opens news or video url of the event. + """ + if event.button.id == "explore-btn" and self.event.news_url: + webbrowser.open(self.event.news_url, new=2) + elif event.button.id == "watch-btn" and self.event.video_url: + webbrowser.open(self.event.video_url, new=2) + + def on_mount(self) -> None: + """Event handler for when EventWidget is being mounted.""" + # Set a delimeter as a gray border at the bottom of each widget. + self.styles.border_bottom = ("hkey", "gray") + + def compose(self) -> ComposeResult: + """Creates a child widget of EventWidget.""" + yield Static(f"{self.event.name}\n") + yield Static(f"{self._date}\n") + yield Static(f"{self.event.description}\n", classes="bg-text") + yield Horizontal( + Button( + "Explore", + id="explore-btn", + variant="primary", + disabled=self._explore_disabled, + ), + Button( + "Watch", id="watch-btn", variant="error", disabled=self._watch_disabled + ), + classes="event-buttons", + ) diff --git a/skylab/widgets/launch_widget.py b/skylab/widgets/launch_widget.py new file mode 100644 index 0000000..e8bd9db --- /dev/null +++ b/skylab/widgets/launch_widget.py @@ -0,0 +1,75 @@ +"""Launch widget of Skylab app.""" + +import datetime +import urllib.parse +import webbrowser + +from textual.app import ComposeResult +from textual.containers import Container, Vertical +from textual.widgets import Button, Static + +from skylab import settings +from skylab.models.launch_models import Launch +from skylab.widgets.time_widget import TimeDisplay + + +class LaunchWidget(Static): + """Launch object Static.""" + + YOUTUBE_URL = "https://www.youtube.com" + + def __init__(self, *args, **kwargs) -> None: + """Initialize LaunchWidget.""" + # remove launch object from kwargs before doing super() + self.launch: Launch = kwargs.pop("launch") + super().__init__(*args, **kwargs) + self._net = datetime.datetime.fromisoformat(self.launch.net[:-1]) + self._launch_time = Static( + f"{settings.LOCAL_TIMEZONE.localize(self._net)}\n", classes="bg-text" + ) + self._launch_provider = Static(f"{self.launch.launch_service_provider.name}\n") + self._rocket_name = Static(f"{self.launch.rocket.full_name}\n") + self._launch_pad = Static( + f"{self.launch.pad.name}, {self.launch.pad.address}\n", classes="bg-text" + ) + self._description = Static( + f"{self.launch.mission.description}", classes="bg-text" + ) + + def on_mount(self) -> None: + """Set height of the widget depending on description lenght on mount.""" + descr_len = len(self.launch.mission.description) + if descr_len < 70: + self.styles.height = 13 + elif descr_len < 160: + self.styles.height = 14 + elif descr_len < 300: + self.styles.height = 16 + else: + self.styles.height = 19 + + def on_button_pressed(self) -> None: + """ + Event handler called when a button is pressed. + On press creates a youtube search query for upcoming launch. + """ + query = f"{self.launch.name} {self._net}" + launch_url = ( + f"{self.YOUTUBE_URL}/results?search_query={urllib.parse.quote(query)}" + ) + webbrowser.open(launch_url, new=2) + + def compose(self) -> ComposeResult: + """Creates a child widget of LaunchWidget.""" + yield Container( + self._launch_provider, + self._rocket_name, + self._launch_time, + self._launch_pad, + self._description, + ) + yield Vertical( + TimeDisplay(launch_time=self._net, id="launch-counter", classes="bg-text"), + Button("Watch", id="watch-button"), + id="right-launch-widget", + ) diff --git a/skylab/widgets/time_widget.py b/skylab/widgets/time_widget.py new file mode 100644 index 0000000..fa40cd2 --- /dev/null +++ b/skylab/widgets/time_widget.py @@ -0,0 +1,41 @@ +"""Time till lauch countdown widget.""" + +import datetime + +from textual.reactive import reactive +from textual.widgets import Static + +from skylab import settings + + +class TimeDisplay(Static): + """Time till launch Static.""" + + time_to_launch: reactive[datetime.timedelta] = reactive(datetime.timedelta(0)) + + def __init__(self, *args, **kwargs) -> None: + """Initialize TimeDisplay.""" + # Remove passed time_to_launch var before calling super() + self._launch_time = kwargs.pop("launch_time") + offset = self._launch_time.astimezone(settings.LOCAL_TIMEZONE).utcoffset() + self._offset = offset if offset else datetime.timedelta(0) + super().__init__(*args, **kwargs) + + def on_mount(self) -> None: + """On widget mount sets interval to call update_tim every second.""" + self.time_to_launch = self._launch_time - datetime.datetime.now() + self._offset + self.set_interval(1, self.update_time) + + def update_time(self) -> None: + """Updates time_to_launch value.""" + self.time_to_launch = self._launch_time - datetime.datetime.now() + self._offset + + def watch_time_to_launch(self, time_to_launch: datetime.timedelta) -> None: + """ + Updates screen value of time_to_launch every time the value changes + and removes milliseconds from time_to_launch value to display. + """ + formated_time = datetime.timedelta( + days=time_to_launch.days, seconds=time_to_launch.seconds + ) + self.update(f"{formated_time}") diff --git a/tests/test_api.py b/tests/test_api.py index 783eb34..64d1525 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,8 +1,8 @@ import pytest import requests -from skylab.api import get_upcoming_laucnhes, launch_factory -from skylab.models import Launch +from skylab.api import SpaceDevApi +from skylab.models.launch_models import Launch MOCK_LAUNCHES = { "results": [ @@ -27,23 +27,25 @@ @pytest.fixture -def mock_get_upcoming_launches(monkeypatch): +def mock_get_request(monkeypatch): def mock_get(*args, **kwargs): response = requests.Response() response.status_code = 200 response.json = lambda: MOCK_LAUNCHES return response - monkeypatch.setattr(requests, "get", mock_get) + monkeypatch.setattr(requests.Session, "get", mock_get) -def test_get_upcoming_launches(mock_get_upcoming_launches): - results = get_upcoming_laucnhes("https://mockurl.com") +def test_get_upcoming_launches(mock_get_request): + api = SpaceDevApi() + results = api.get_request("https://mockurl.com") assert results == MOCK_LAUNCHES -def test_launch_factory(mock_get_upcoming_launches): - launches = launch_factory() +def test_launch_factory(mock_get_request): + api = SpaceDevApi() + launches = api.launch_factory() assert isinstance(launches, list) assert len(launches) == len(MOCK_LAUNCHES["results"]) for launch in launches: diff --git a/tests/test_models.py b/tests/test_models.py index e5f344d..44e61fc 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,10 @@ -from skylab.models import Launch, LaunchPad, LaunchProvier, Mission, Rocket +from skylab.models.launch_models import ( + Launch, + LaunchPad, + LaunchProvier, + Mission, + Rocket, +) def test_launch_default_values(): diff --git a/tests/test_skylab.py b/tests/test_skylab.py index 8c84856..776d396 100644 --- a/tests/test_skylab.py +++ b/tests/test_skylab.py @@ -1,8 +1,8 @@ import pytest from textual.widgets import Static -from skylab.models import Launch -from skylab.skylab import LaunchWidget +from skylab.models.launch_models import Launch +from skylab.widgets.launch_widget import LaunchWidget CORRECT_MOCK_LAUNCHES = { "results": { @@ -16,7 +16,7 @@ } -class TestLaunchWidget: +class Test: @pytest.fixture def launch(self): launch_json = CORRECT_MOCK_LAUNCHES["results"]