diff --git a/TODO.md b/TODO.md index 604ad72..d155077 100644 --- a/TODO.md +++ b/TODO.md @@ -39,4 +39,6 @@ Eliminar codigo muerto POner para clasificar en vacia o llena Poner columna con dias que faltan poner tabla con el dia de hoy -switchear tabla a tabla a surf forecast \ No newline at end of file +switchear tabla a tabla a surf forecast +Refactorizar, tipar +# TODO posiblemente hacer una clase para calcular los spot_names \ No newline at end of file diff --git a/apt.keys b/apt.keys deleted file mode 100644 index 8680932..0000000 --- a/apt.keys +++ /dev/null @@ -1,232 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.2.2 (GNU/Linux) - -mQGiBEXwb0YRBADQva2NLpYXxgjNkbuP0LnPoEXruGmvi3XMIxjEUFuGNCP4Rj/a -kv2E5VixBP1vcQFDRJ+p1puh8NU0XERlhpyZrVMzzS/RdWdyXf7E5S8oqNXsoD1z -fvmI+i9b2EhHAA19Kgw7ifV8vMa4tkwslEmcTiwiw8lyUl28Wh4Et8SxzwCggDcA -feGqtn3PP5YAdD0km4S4XeMEAJjlrqPoPv2Gf//tfznY2UyS9PUqFCPLHgFLe80u -QhI2U5jt6jUKN4fHauvR6z3seSAsh1YyzyZCKxJFEKXCCqnrFSoh4WSJsbFNc4PN -b0V0SqiTCkWADZyLT5wll8sWuQ5ylTf3z1ENoHf+G3um3/wk/+xmEHvj9HCTBEXP -78X0A/0Tqlhc2RBnEf+AqxWvM8sk8LzJI/XGjwBvKfXe+l3rnSR2kEAvGzj5Sg0X -4XmfTg4Jl8BNjWyvm2Wmjfet41LPmYJKsux3g0b8yzQxeOA4pQKKAU3Z4+rgzGmf -HdwCG5MNT2A5XxD/eDd+L4fRx0HbFkIQoAi1J3YWQSiTk15fw7RMR29vZ2xlLCBJ -bmMuIExpbnV4IFBhY2thZ2UgU2lnbmluZyBLZXkgPGxpbnV4LXBhY2thZ2VzLWtl -eW1hc3RlckBnb29nbGUuY29tPohjBBMRAgAjAhsDBgsJCAcDAgQVAggDBBYCAwEC -HgECF4AFAkYVdn8CGQEACgkQoECDD3+sWZHKSgCfdq3HtNYJLv+XZleb6HN4zOcF -AJEAniSFbuv8V5FSHxeRimHx25671az+uQINBEXwb0sQCACuA8HT2nr+FM5y/kzI -A51ZcC46KFtIDgjQJ31Q3OrkYP8LbxOpKMRIzvOZrsjOlFmDVqitiVc7qj3lYp6U -rgNVaFv6Qu4bo2/ctjNHDDBdv6nufmusJUWq/9TwieepM/cwnXd+HMxu1XBKRVk9 -XyAZ9SvfcW4EtxVgysI+XlptKFa5JCqFM3qJllVohMmr7lMwO8+sxTWTXqxsptJo -pZeKz+UBEEqPyw7CUIVYGC9ENEtIMFvAvPqnhj1GS96REMpry+5s9WKuLEaclWpd -K3krttbDlY1NaeQUCRvBYZ8iAG9YSLHUHMTuI2oea07Rh4dtIAqPwAX8xn36JAYG -2vgLAAMFB/wKqaycjWAZwIe98Yt0qHsdkpmIbarD9fGiA6kfkK/UxjL/k7tmS4Vm -CljrrDZkPSQ/19mpdRcGXtb0NI9+nyM5trweTvtPw+HPkDiJlTaiCcx+izg79Fj9 -KcofuNb3lPdXZb9tzf5oDnmm/B+4vkeTuEZJ//IFty8cmvCpzvY+DAz1Vo9rA+Zn -cpWY1n6z6oSS9AsyT/IFlWWBZZ17SpMHu+h4Bxy62+AbPHKGSujEGQhWq8ZRoJAT -G0KSObnmZ7FwFWu1e9XFoUCt0bSjiJWTIyaObMrWu/LvJ3e9I87HseSJStfw6fki -5og9qFEkMrIrBCp3QGuQWBq/rTdMuwNFiEkEGBECAAkFAkXwb0sCGwwACgkQoECD -D3+sWZF/WACfeNAu1/1hwZtUo1bR+MWiCjpvHtwAnA1R3IHqFLQ2X3xJ40XPuAyY -/FJG -=Quqp ------END PGP PUBLIC KEY BLOCK----- ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx -BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS -pJT+0m2SgUNhLAn1WY/iNJGNaMl7lgUnaP+/ZsSNT9hyTBiH3Ev5VvAtMGhVI/u8 -P0EtTjXp4o2U+VqFTBGmZ6PJVhCFjZUeRByloHw8dGOshfXKgriebpioHvU8iQ2U -GV3WNIirB2Rq1wkKxXJ/9Iw+4l5m4GmXMs7n3XaYQoBj28H86YA1cYWSm5LR5iU2 -TneI1fJ3vwF2vpSXVBUUDk67PZhg6ZwGRT7GFWskC0z8PsWd5jwK20mA8EVKq0vN -BFmMK6i4fJU+ux17Rgvnc9tDSCzFZ1/4f43EZ41uTmmNXIDsaPCqwjvSS5ICadt2 -xeqTWDlzONUpOs5yBjF1cfJSdVxsfshvln2JXUwgIdKl4DLbZybuNFXnPffNLb2v -PtRJHO48O2UbeXS8n27PcuMoLRd7+r7TsqG2vBH4t/cB/1vsvWMbqnQlaJ5VsjeW -Tp8Gv9FJiKuU8PKiWsF4EGR/kAFyCB8QbJeQ6HrOT0CXLOaYHRu2TvJ4taY9doXn -98TgU03XTLcYoSp49cdkkis4K+9hd2dUqARVCG7UVd9PY60VVCKi47BVKQARAQAB -tFRHb29nbGUgSW5jLiAoTGludXggUGFja2FnZXMgU2lnbmluZyBBdXRob3JpdHkp -IDxsaW51eC1wYWNrYWdlcy1rZXltYXN0ZXJAZ29vZ2xlLmNvbT6JAk4EEwEIADgC -GwMCHgECF4AWIQTrTBv9TwQvbd3M7JF3IfY704tHlgUCVwyM0wULCQgHAgYVCgkI -CwIEFgIDAQAKCRB3IfY704tHlkGrD/9aIOPxoABbhHDa+GbM1XHSeV99q2UOIsYc -A5Jg3k2+Vbjr/006cL9Kk+rdbruZJtERo2z+HVVhkJisvySbsd0UbWfiY5AdHzNP -azpitbX9cNYi0ghDZsD5UgP3cWdx21BJPO0v9PBG9U4z1TQ+pmsQphtNzMC4tK+A -H/7WTXnVPzKXTYziIEIPgHeassSj7Yfwa8kLiBR5tAehHDNNMi/mMf4d6a+wO46x -hhRx/BLjoaIxsZw9f5VxDAqGbCrW8IccwJX8vTc89y+6vpzSurdqYrplZWGpcnfT -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 -47hwiKc+VTQGvCZqs8eT+pbnw1Recd13J9Ny7bM= -=OrhN ------END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/front/one/table.py b/front/one/table.py index fdd316c..b9ca2bb 100644 --- a/front/one/table.py +++ b/front/one/table.py @@ -1,13 +1,11 @@ import streamlit as st -import time from utils import final_forecast_format, construct_date_selection_list -from multi import multithread from scrapers.windguru import Windguru from scrapers.tides import TidesScraper import altair as alt import polars as pl from datetime import datetime, timedelta -from urls.windguru import WINDGURU_URLS +from st_aggrid import AgGrid, GridOptionsBuilder DEFAULT_MIN_WAVE_PERIOD = 0 DEFAULT_WAVE_HEIGHT = 0.0 @@ -29,16 +27,16 @@ def plot_graph(variable): st.header(f"{variable} per day", divider="rainbow") data = st.session_state.forecast_df chart = ( - alt.Chart(data) + alt.Chart(data.to_pandas()) .mark_line(strokeWidth=3, point=True) .encode( x="datetime:T", y=alt.Y(f"{variable}:Q", impute=alt.ImputeParams(value=None)), color="spot_name:N", - detail="date_dt:T", + detail="date:T", tooltip=[ - alt.Tooltip("date_dt:T", format="%d/%m/%Y", title="Date"), - alt.Tooltip("time_cor:N", title="Time"), + alt.Tooltip("date:T", format="%d/%m/%Y", title="Date"), + alt.Tooltip("time_graph:N", title="Time"), "energy:Q", "spot_name:N", "wave_height:Q", @@ -103,12 +101,11 @@ def plot_selected_wave_energy(): @st.cache_data(ttl=3600) def load_windguru_forecast(): - url = "https://www.windguru.cz/49328" tide_scraper = TidesScraper() tides = tide_scraper.tasks() - + id_spot = "49328" windguru = Windguru() - df = windguru.scrape(url, tides) + df = windguru.scrape_with_request(id_spot, tides) df = final_forecast_format(df) return df @@ -198,26 +195,11 @@ def plot_forecast_as_table(): ) st.session_state.forecast_df = st.session_state.forecast_df.filter(mask) - date = st.session_state.forecast_df["datetime"].dt.date().to_list() - time = [ - element.replace(":", r"\:") - for element in st.session_state.forecast_df["time"].to_list() - ] - st.session_state.forecast_df = st.session_state.forecast_df.with_columns( - pl.Series(name="date_dt", values=date) - ) - - st.session_state.forecast_df = st.session_state.forecast_df.with_columns( - pl.Series(name="time_cor", values=time) - ) plot_graph("energy") grouped_data = st.session_state.forecast_df.group_by("spot_name").agg( pl.col("datetime").min().alias("datetime") ) - if "datetime" in grouped_data.columns: - grouped_data = grouped_data.sort("datetime", descending=False) - else: - st.warning("'datetime' column not found, skipping sort.") + grouped_data = grouped_data.sort("datetime", descending=False) with st.container(): for i in range(len(grouped_data)): @@ -227,16 +209,47 @@ def plot_forecast_as_table(): ) with st.expander(f"Spot: {spot_name}"): - forecast_df_dropped = group_df.drop("spot_name") + forecast_df_dropped = ( + group_df.drop("spot_name") + .drop("date") + .drop("time") + .drop("time_graph") + ) forecast_df_dropped.sort("datetime", descending=False) forecast_to_plot = forecast_df_dropped.drop("datetime") forecast_columns = [ - f"**{column.upper()}**" for column in forecast_to_plot.columns + column.upper() for column in forecast_to_plot.columns ] rotated_df = forecast_to_plot.transpose(include_header=False) s = pl.Series("column names", forecast_columns) rotated_df.insert_column(0, s) - st.dataframe(rotated_df, use_container_width=True, hide_index=True) + rotated_df_pd = rotated_df.to_pandas() + + gb = GridOptionsBuilder.from_dataframe(rotated_df_pd) + gb.configure_default_column( + wrapText=True, autoHeight=True, editable=False + ) + gb.configure_grid_options( + domLayout="normal", + ) + gb.configure_column( + "column names", pinned="left", cellStyle={"fontWeight": "bold"} + ) + + grid_options = gb.build() + + column_defs = grid_options["columnDefs"] + + for col in column_defs: + col["headerName"] = "" + + AgGrid( + rotated_df_pd, + gridOptions=grid_options, + fit_columns_on_grid_load=True, + theme="alpine", + enable_enterprise_modules=False, + ) diff --git a/packages.txt b/packages.txt deleted file mode 100644 index 2cc111a..0000000 --- a/packages.txt +++ /dev/null @@ -1,2 +0,0 @@ -google-chrome-stable -locales-all \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 2f48444..07ce264 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,26 +2,25 @@ [[package]] name = "altair" -version = "5.4.0" -description = "Vega-Altair: A declarative statistical visualization library for Python." +version = "4.2.2" +description = "Altair: A declarative statistical visualization library for Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "altair-5.4.0-py3-none-any.whl", hash = "sha256:86be974867007cfdf5c92d6f89926535546a4d00e0ea6c1745ef4d5937aad9df"}, - {file = "altair-5.4.0.tar.gz", hash = "sha256:27c69e93d85b7bb3c98fa3626ef7e6bc6939a1466a55a8f8bf68c4bff31cf030"}, + {file = "altair-4.2.2-py3-none-any.whl", hash = "sha256:8b45ebeaf8557f2d760c5c77b79f02ae12aee7c46c27c06014febab6f849bc87"}, + {file = "altair-4.2.2.tar.gz", hash = "sha256:39399a267c49b30d102c10411e67ab26374156a84b1aeb9fcd15140429ba49c5"}, ] [package.dependencies] +entrypoints = "*" jinja2 = "*" jsonschema = ">=3.0" -narwhals = ">=1.1.0" -packaging = "*" -typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""} +numpy = "*" +pandas = ">=0.18" +toolz = "*" [package.extras] -all = ["altair-tiles (>=0.3.0)", "anywidget (>=0.9.0)", "numpy", "pandas (>=0.25.3)", "pyarrow (>=11)", "vega-datasets (>=0.9.0)", "vegafusion[embed] (>=1.6.6)", "vl-convert-python (>=1.6.0)"] -dev = ["geopandas", "hatch", "ibis-framework[polars]", "ipython[kernel]", "mistune", "mypy", "pandas (>=0.25.3)", "pandas-stubs", "polars (>=0.20.3)", "pytest", "pytest-cov", "pytest-xdist[psutil] (>=3.5,<4.0)", "ruff (>=0.5.7)", "types-jsonschema", "types-setuptools"] -doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx (>=8.0.0)", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] +dev = ["black", "docutils", "flake8", "ipython", "m2r", "mistune (<2.0.0)", "pytest", "recommonmark", "sphinx", "vega-datasets"] [[package]] name = "annotated-types" @@ -356,6 +355,17 @@ click = ">=8.0.0,<9" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} pathspec = ">=0.9.0" +[[package]] +name = "entrypoints" +version = "0.4" +description = "Discover and load entry points from installed packages." +optional = false +python-versions = ">=3.6" +files = [ + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, +] + [[package]] name = "gitdb" version = "4.0.11" @@ -1257,6 +1267,17 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-decouple" +version = "3.8" +description = "Strict separation of settings from code." +optional = false +python-versions = "*" +files = [ + {file = "python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f"}, + {file = "python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66"}, +] + [[package]] name = "pytz" version = "2024.1" @@ -1541,6 +1562,23 @@ watchdog = {version = ">=2.1.5,<7", markers = "platform_system != \"Darwin\""} [package.extras] snowflake = ["snowflake-connector-python (>=2.8.0)", "snowflake-snowpark-python[modin] (>=1.17.0)"] +[[package]] +name = "streamlit-aggrid" +version = "1.0.5" +description = "Streamlit component implementation of ag-grid" +optional = false +python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" +files = [ + {file = "streamlit_aggrid-1.0.5-py3-none-any.whl", hash = "sha256:ac0a58c1d39418d139da5623c4c8a0a3aa86463c217a41c837e2ef52c8537a34"}, + {file = "streamlit_aggrid-1.0.5.tar.gz", hash = "sha256:12e17f88d66e110e5d68504614a4b933f7dca31e40448396252d889a10e761b6"}, +] + +[package.dependencies] +altair = "<5" +pandas = ">=1.2" +python-decouple = ">=3.6,<4.0" +streamlit = ">=0.87.0" + [[package]] name = "tabulate" version = "0.9.0" @@ -1804,4 +1842,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "3e8a0cbc1b2f35ff74435142b103e5b072b86e337678f7cd45d19f415cc46717" +content-hash = "168d344a4ee7507418ba8ebbe5eebe52a489c327aa56d2666276325c3492dfe8" diff --git a/pyproject.toml b/pyproject.toml index 4bfd04f..90355df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" -altair = "5.4.0" annotated-types = "0.6.0" attrs = "24.2.0" beautifulsoup4 = "4.12.2" @@ -76,6 +75,7 @@ wsproto = "1.2.0" zipp = "3.18.1" pytest = "^8.3.3" tabulate = "^0.9.0" +streamlit-aggrid = "^1.0.5" [build-system] diff --git a/scrapers/windguru.py b/scrapers/windguru.py index aae3174..9bcf88c 100644 --- a/scrapers/windguru.py +++ b/scrapers/windguru.py @@ -1,24 +1,20 @@ -from bs4 import BeautifulSoup -from typing import Dict +from datetime import datetime, timezone import polars as pl from utils import ( - rename_key, generate_tides, generate_nearest_tides, generate_tide_percentages, generate_energy, - render_html, - generate_datetimes, - filter_spot_dataframe, create_date_name_column, create_direction_predominant_column, generate_spot_names, - datestr_to_frontend_format, + datetime_to_frontend_str, + degrees_to_direction, + generate_forecast_moments, ) -import re -from APIS.discord_api import DiscordBot import locale import requests +import math locale.setlocale(locale.LC_TIME, "es_ES.UTF-8") @@ -27,178 +23,125 @@ class Windguru(object): def __init__(self): pass - def beach_request(self, url): - r_text = render_html(url=url, tag_to_wait="table.tabulka", timeout=60 * 1000) - return BeautifulSoup(r_text, "html.parser") + def generate_rundef( + self, + forecast_start=0, + forecast_end=240, + long_range_start=243, + long_range_end=384, + ): + now_dt = datetime.now(timezone.utc) + + if now_dt.hour >= 12: + model_run_time = now_dt.replace(hour=12, minute=0, second=0, microsecond=0) + else: + model_run_time = now_dt.replace(hour=0, minute=0, second=0, microsecond=0) + init_time_str = model_run_time.strftime("%Y%m%d%H") + rundef = f"{init_time_str}x{forecast_start}x{forecast_end}x{forecast_start}x{forecast_end}-{init_time_str}x{long_range_start}x{long_range_end}x{long_range_start}x{long_range_end}" + + return rundef + + def get_wind_from_api(self, id_spot: str): + rundef = self.generate_rundef() + headers = { + "sec-ch-ua-platform": '"Windows"', + "Referer": "https://www.windguru.cz/", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "sec-ch-ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + "sec-ch-ua-mobile": "?0", + } - def format_forecast(self, forecast: Dict, tides: dict) -> Dict: - forecast = rename_key(forecast, "tabid_0_0_dates", "datetime") - forecast = rename_key(forecast, "tabid_0_0_SMER", "wind_direction_raw") - forecast = rename_key(forecast, "tabid_0_0_HTSGW", "wave_height") - forecast = rename_key(forecast, "tabid_0_0_DIRPW", "wave_direction_raw") - forecast = rename_key(forecast, "tabid_0_0_PERPW", "wave_period") - forecast = rename_key(forecast, "tabid_0_0_WINDSPD", "wind_speed") - forecast = rename_key(forecast, "tabid_0_0_TMPE", "temperature") + params = { + "q": "forecast", + "id_model": "3", + "rundef": rundef, + "id_spot": id_spot, + "WGCACHEABLE": "21600", + "cachefix": "29.123x-13.542x-1", + } - forecast["wind_direction"] = [ - self.parse_text_from_text(element) - for element in forecast["wind_direction_raw"] - ] - forecast["wave_direction"] = [ - self.parse_text_from_text(element) - for element in forecast["wave_direction_raw"] - ] - forecast["wind_direction_degrees"] = [ - self.parse_number_from_text(element) - for element in forecast["wind_direction_raw"] - ] - forecast["wave_direction_degrees"] = [ - self.parse_number_from_text(element) - for element in forecast["wave_direction_raw"] - ] + return requests.get( + "https://www.windguru.net/int/iapi.php", params=params, headers=headers + ) - forecast["date"] = [ - datestr_to_frontend_format(dt.split(".")[0]) for dt in forecast["datetime"] + def get_waves_from_api(self, id_spot: str): + rundef = self.generate_rundef() + headers = { + "sec-ch-ua-platform": '"Windows"', + "Referer": "https://www.windguru.cz/", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", + "sec-ch-ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', + "sec-ch-ua-mobile": "?0", + } + + params = { + "q": "forecast", + "id_model": "84", + "rundef": rundef, + "id_spot": id_spot, + "WGCACHEABLE": "21600", + "cachefix": "29.209x-13.676x-1", + } + + return requests.get( + "https://www.windguru.net/int/iapi.php", params=params, headers=headers + ) + + def scrape_with_request(self, id_spot: str, tides: dict): + forecast = {} + waves_response = self.get_waves_from_api(id_spot) + wind_response = self.get_wind_from_api(id_spot) + + waves_data = waves_response.json() + wind_data = wind_response.json() + + wind_speed = wind_data.get("fcst").get("WINDSPD") + wind_direction_degrees = wind_data.get("fcst").get("WINDDIR") + wave_height = waves_data.get("fcst").get("HTSGW") + wave_direction_degrees = waves_data.get("fcst").get("DIRPW") + wave_period = waves_data.get("fcst").get("PERPW") + initstamp = int(wind_data.get("fcst").get("initstamp")) + hours = wind_data.get("fcst").get("hours") + + moments = generate_forecast_moments(initstamp, hours) + + forecast["wind_speed"] = [math.ceil(element) for element in wind_speed] + forecast["wind_direction_degrees"] = wind_direction_degrees + forecast["wave_height"] = [math.ceil(element) for element in wave_height] + forecast["wave_direction_degrees"] = wave_direction_degrees + forecast["wave_period"] = [math.ceil(element) for element in wave_period] + forecast["datetime"] = moments + forecast["date"] = [moment.date() for moment in moments] + forecast["date_friendly"] = [ + datetime_to_frontend_str(moment) for moment in moments ] - forecast["time"] = [ - dt.split(".")[1].replace("h", ":00") for dt in forecast["datetime"] + forecast["time"] = [moment.time() for moment in moments] + time_friendly = [moment.strftime("%H:%M") for moment in moments] + forecast["time_friendly"] = time_friendly + forecast["time_graph"] = [ + element.replace(":", r"\:") for element in time_friendly ] - forecast["datetime"] = generate_datetimes(forecast["date"], forecast["time"]) - forecast["wave_period"] = self.format_dict_digit_all_values( - forecast, "wave_period", "int" - ) - forecast["wave_height"] = self.format_dict_digit_all_values( - forecast, "wave_height", "float" + forecast["wind_direction_predominant"] = create_direction_predominant_column( + wind_direction_degrees ) - forecast["wind_speed"] = self.format_dict_digit_all_values( - forecast, "wind_speed", "float" + forecast["wind_direction"] = [ + degrees_to_direction(element) for element in wind_direction_degrees + ] + forecast["wave_direction_predominant"] = create_direction_predominant_column( + wave_direction_degrees ) + forecast["wave_direction"] = [ + degrees_to_direction(element) for element in wave_direction_degrees + ] + forecast["energy"] = generate_energy(wave_height, wave_period) + forecast["tide"] = generate_tides(tides, forecast["datetime"]) forecast["nearest_tide"] = generate_nearest_tides(tides, forecast["datetime"]) forecast["tide_percentage"] = generate_tide_percentages( tides, forecast["datetime"] ) - forecast["energy"] = generate_energy( - forecast["wave_height"], forecast["wave_period"] - ) - forecast["date_name"] = create_date_name_column(forecast["datetime"]) - forecast["wind_direction_predominant"] = create_direction_predominant_column( - forecast["wind_direction_degrees"] - ) - forecast["wave_direction_predominant"] = create_direction_predominant_column( - forecast["wave_direction_degrees"] - ) forecast["spot_name"] = generate_spot_names(forecast) - return forecast - - def get_dataframe_from_soup(self, soup: BeautifulSoup, tides: dict) -> Dict: - forecast = {} - table = soup.find("table", class_="tabulka") - tablebody = table.find("tbody") - rows = tablebody.find_all("tr") - - if len(rows) == 0: - print(soup) - raise Exception("No se encontro la tabla") - - for row in rows: - cells = row.find_all("td") - id = row["id"] - if id in [ - "tabid_0_0_SMER", - "tabid_0_0_dates", - "tabid_0_0_HTSGW", - "tabid_0_0_DIRPW", - "tabid_0_0_PERPW", - "tabid_0_0_WINDSPD", - "tabid_0_0_TMPE", - ]: - forecast[id] = [] - for cell in cells: - if ("SMER" in id) | ("DIRPW" in id): - try: - value = cell.select("span")[0]["title"] - except Exception as e: - value = "NAN (-69°)" - else: - value = cell.get_text().replace("-", "-69") - forecast[id].append(value) - if forecast != {}: - total_records = len(max(forecast.items(), key=lambda item: len(item[1]))[1]) - forecast = self.format_forecast(forecast, tides) - return pl.DataFrame(forecast) - return pl.DataFrame() - - def parse_text_from_text(self, text): - return text.split(" ")[0].replace("O", "W") - - def parse_number_from_text(self, text): - pattern = r"(\d+)°" - match = re.search(pattern, text, re.IGNORECASE) - if match: - return float(match.group(1)) - - def format_dict_digit_all_values(self, forecast, forecast_value, digit_type): - if digit_type == "int": - return [ - int(forecast[forecast_value][i]) - for i in range(len(forecast[forecast_value])) - ] - elif digit_type == "float": - return [ - float(forecast[forecast_value][i]) - for i in range(len(forecast[forecast_value])) - ] - - def handle_windguru_alerts(self, df: pl.DataFrame): - famara_df = filter_spot_dataframe("famara", df, three_near_days=True) - tiburon_df = filter_spot_dataframe("tiburon", df, three_near_days=True) - barcarola_df = filter_spot_dataframe("barcarola", df, three_near_days=True) - bastian_df = filter_spot_dataframe("bastian", df, three_near_days=True) - punta_df = filter_spot_dataframe("punta_mujeres", df, three_near_days=True) - arrieta_df = filter_spot_dataframe("arrieta", df, three_near_days=True) - spots_df = [ - # caleta_caballo_df, - tiburon_df, - barcarola_df, - bastian_df, - punta_df, - arrieta_df, - famara_df, - ] - for df in spots_df: - if not df.is_empty(): - # result_df = df.filter(df) - discort_bot = DiscordBot() - for row in df.rows(named=True): - discort_bot.waves_alert( - f"windguru - **{row['spot_name'].upper()}**: {row['date_name']}, día {row['date']} a las {row['time']}, una altura de {row['wave_height']}, un periodo de {row['wave_period']} y una direccion del viento de {row['wind_direction']} y una direccion de la ola de {row['wave_direction']}, velocidad del viento de {row['wind_speed']} y la marea estará {row['tide']}" - ) - return - - def windguru_request(self): - import requests - - headers = { - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "Accept-Language": "es-ES,es;q=0.9", - "Cache-Control": "max-age=0", - "Connection": "keep-alive", - # 'Cookie': 'langc=es-; deviceid=9ec2d8a3c0ef8b0d0a8281e69cf4a8fb; _sharedID=1bc832d1-5973-44e9-a1f9-56075be8bdcd; snconsent=eyJwdWJsaXNoZXIiOjAsInZlbmRvciI6MywiZ2xDb25zZW50cyI6IiIsImN2Q29uc2VudHMiOnt9fQ.AlMAKwAuADcAPQBGAFMAWQBdAGwAdQB6AHwAhwCPAJAAkwCVAJ8AwADEANMA5ADmAO8BAwEKAR4BIwE3AT4BQAFCAUMBRwFvAXMBgQGKAY0BlwGfAagBrgG0Ab0BxQHmAesB7gHvAgoCCwIcAiYCLwIwAjgCPgJAAkgCSwJPAuEDIgMjAzQDNQNHA2ADgwOIA5oDowOqA9MD1QPZA-sEAwQHBBAEFgQbBB0EKwREBEcESQRLBFMEbwR3BH0EgASKBI4EogSkBLUEvwTKBMsEzgTkBPQE9gT8BQQFCgUVBRsFIAVBBUwFVAVfBXsFhwWIBY0FjwWgBakFrwXXBegF7AX1BgQGDAYTBhYGIgYpBisGLwYwBjcGQwZQBmYGcwZ1BnsGgwaNBo4GkgahBqMGpwawBrQGuQa9BsQG0QbWBuUG9gb6BwgHEgchByMHKAcuBzAHMgczBzUHQwdKB04HVgdYB2EHawd9B4kHlgeYB6oHqwesB68HsAexB7oHwQfDB9MH2AfrB_MH9wf_CAQICAgQCBQIGAgaCCgIKgg3CDsIPQhDCEwIUghVCFcIWQhcCGMIZghsCHYIgQiHCIoInQilCKgIqwisCK4IsQi6CM0I5wjqCPQJAQkFCQgJDAkSCRUJGAkbCR4JHwkgCSEJJwkyCTUJNgk3CUIJSAlJCVMJYAljCWUJZwlrCW4JcAlyCXkJiAmPCZ0JoQmkCagJrQmxCbQJtgm4Cb0JwgnFCc4J1QneCd8J5AnnCe4J-AoDCgQKBwoICgkKCwoMCg8KEQoXChgKJAosCi0KMAoxCjIKNAo2Cj0KRApFCkkKTApSClMKVQpWCloKWwpcCmAKYQpiCmQKZQptCm4KdQp5CnwKfwqCCocKigqZCpoKqQqzCs8K0ArSCtQK4ArjCucK6AruCvEK9Qr8Cv0LAAsBCwULBgsLCw4LDwsSCxYLFwscCx4LIQsiCyQLJgssCy4LLwsxCzMLNQs5CzoLOws8Cz4LQAtBC0ILQwtEC0YLRwtIC0kLSwtNC04LTwtRC1ILVAtVC1wLXQtkC2ULZgtnC2gLagtrC28LcQtyC3MLfAt9C4MLhQuGC4wLjguRC5MLlAuVC5YLmAudC58LowukC6ULpwupC6oLqwuyC7MLtQu3C7gLugu7C70LwAvBC8ILxAvIC8kLygvLC9EL1AvaC94L4wvoC-wL7QvvC_IL8wv3C_oL_Av-DAEMAgwDDAQMBQwRDBIMFQwWDBcMGQwbDBwMIgwlDCgMLQwvDDYMNww4DDoMPwxADEkMTgxPDFIMUwxbDF8MZAxlDG4MbwxwDHEMcwx0DHUMdgx6DHwMiQyKDIsMjgyPDJEMkwyWDJcMmQyaDJsMnAyeDJ8MogyjDKQMpQymDKgMrAytDLIMswy1DLkMvAzGDMgM0QzYDNoM3AzdDOAM4wzkDOoM6wztDPIM8wz0DPYM_A0ADQINAw3LDpMO9xAjEbMSFxJ7Et8UbxsTHEMelx77IuMmAyf3KYcqTysXLQsyHzVANaM3nTf8PXNBv0IjUvFZ92SDZUtlr2jPbFNst21_cDtwn3O_e49_E4NfhbeHRw; euconsent-v2=CQHqmJgQHqmJgDlBWAESBIFsAP_gAEPgAATIKdNV_G__bXlv-X736ftkeY1f9_h77sQxBhfJs-4FzLvW_JwX32EzNE36tqYKmRIAu3bBIQNtGJjUTVChaogVrzDsaE2coTtKJ-BkiHMRc2dYCF5vm4tj-QKZ5vr_91d52R_t7dr-3dzyz5Vnv3a9_-b1WJidK5-tH_v_bROb-_I-9_x-_4v8_N_rE2_eT1t_tWvt739-8tv___f99___________3_-__wU6AJMNCogDLAkJCDQMIIEAKgrCAigQAAAAkDRAQAmDAp2BgEusJEAIAUAAwQAgABRkACAAACABCIAIACgQAAQCBQABgAQDAQAEDAACACwEAgABAdAxTAggUCwASMyIhTAhCASCAlsqEEgCBBXCEIs8AiAREwUAAAAABWAAICwWBxJICVCQQJcQbQAAEACAQQAFCCTkwABAGbLUHgybRlaYBo-YJENMAyAAAA; _ga=GA1.1.1673142863.1730893842; _cc_id=bf375ae509a9208d26c6b2bf1d48187b; _lr_env_src_ats=false; wgcookie=2|||||||||49328||||0|51_0|0|||||||||; ac_cclang=; ac_user_id=acux44vc5752g4872a056f7e24c031d69e605be162ca38bf3d79d676204583d4dfbee9f08beb43c; _au_1d=AU1D-0100-001731523718-L3O7HRF7-Q0AG; _au_last_seen_iab_tcf=1731523718003; _pbjs_userid_consent_data=3524755945110770; _sharedID_cst=JizbLCcsIA%3D%3D; panoramaId_expiry=1733926176381; panoramaId=c3b9cf7059d83a4d9716af5d79f9c8bd038aea7607ab7e50f6f0e15ae75d3161; panoramaIdType=panoIndiv; _sharedID_last=Mon%2C%2009%20Dec%202024%2012%3A51%3A36%20GMT; _ga_2NEY9YDWMB=GS1.1.1733748696.107.0.1733748696.0.0.0; cto_bundle=UKMKKV8wRU05aEp6T29IalglMkZRN1JSQXp2VllVVjhRNiUyQmlkVURLdTJPMFpyRUJvT1ZOTGpiWkZxdDJ3OUJMeVBzNSUyQnBXc3YzJTJCSk5KQSUyQnIlMkJ0SWFHRXBTdWdVMHpGS2tURkFLY2dwcnBnRkdCRmt6eDhOYzVMaTFaU3dDalJYQWtlQ3lneDVPa29uZ0dra1U1SnJBWUUlMkZBa2tHdyUzRCUzRA; __gads=ID=f91c4f474cf41d42:T=1730893845:RT=1733749327:S=ALNI_MYMXr2q4NU1CaVzAhcMS7eE78gfIw; __gpi=UID=00000f6be5ad9fcd:T=1730893845:RT=1733749327:S=ALNI_MbAz8LNc8YTVV96sViwvjL760b5NQ; __eoi=ID=b592dec7ba13116a:T=1730893845:RT=1733749327:S=AA-AfjbAHKdOWULGMncMuL7MhWgx; session=483c20522a93ea0d0b1ba9be469a29c4', - "Referer": "https://www.google.com/", - "Sec-Fetch-Dest": "empty", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "same-origin", - "Upgrade-Insecure-Requests": "1", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", - "sec-ch-ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"Windows"', - } - - return requests.get("https://www.windguru.cz/49328", headers=headers) + forecast["date_name"] = create_date_name_column(forecast["datetime"]) - def scrape(self, url, tides): - soup = self.beach_request(url) - return self.get_dataframe_from_soup(soup, tides) + return pl.DataFrame(forecast) diff --git a/sources.list b/sources.list deleted file mode 100644 index 71b183c..0000000 --- a/sources.list +++ /dev/null @@ -1 +0,0 @@ -deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main \ No newline at end of file diff --git a/utils.py b/utils.py index 21a3283..1618935 100644 --- a/utils.py +++ b/utils.py @@ -1,14 +1,9 @@ -import re, json, math +import json, math from bs4 import BeautifulSoup from requests import Response import polars as pl -from datetime import datetime, date, time, timedelta -from dateutil.relativedelta import relativedelta -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from typing import Dict +from datetime import datetime, date, time, timedelta, timezone +from typing import Dict, List MONTH_MAPPING = { "Ene": "01", @@ -48,21 +43,6 @@ def generate_energy(wave_heights: list, wave_periods: list): ] -def generate_date_range(start_date: datetime, end_date: datetime): - date_list = [] - current_date = start_date - - while current_date <= end_date: - date_list.append(current_date.strftime("%d/%m/%Y")) - current_date += timedelta(days=1) - - return date_list - - -# TODO posiblemente hacer una clase para todo esto, para calcular los spot_names -# TODO quitar float de donde no sea necesario, poner int - - def punta_mujeres_conditions( wind_direction_predominant: str, wave_direction_predominant: str, @@ -746,38 +726,6 @@ def generate_spot_names(forecast: Dict[str, list]) -> list: return spot_names -# margen de 20 grados en N,S,E,O -def angle_to_direction(angle): - angle %= 360 - if 0 <= angle < 10 or angle >= 350: - return "North" - elif 10 <= angle < 80: - return "NorthEast" - elif 80 <= angle < 100: - return "East" - elif 100 <= angle < 170: - return "SouthEast" - elif 170 <= angle < 190: - return "South" - elif 190 <= angle < 260: - return "SouthWest" - elif 260 <= angle < 280: - return "West" - elif 280 <= angle < 350: - return "NorthWest" - - -def export_to_html(filename, response: Response): - soup = BeautifulSoup(response.text, "html.parser") - with open(filename, "w") as file: - file.write(soup.prettify()) - - -def import_html(filename): - with open(filename, "r") as file: - return file.read() - - def combine_df(df1, df2): df = pl.concat([df1, df2]) return df @@ -794,52 +742,6 @@ def convert_datestr_format(datestr): return date_obj.strftime("%d/%m/%Y") -def open_browser(): - my_user_agent = ( - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", - ) - - options = webdriver.ChromeOptions() - options.add_argument("--lang=es") - options.add_argument("--headless") - options.add_argument(f"--user-agent={my_user_agent}") - options.add_argument("--no-sandbox") - options.add_argument("--disable-dev-shm-usage") - options.add_argument("--disable-gpu") - options.add_argument("--window-size=1920x1080") - options.page_load_strategy = "eager" - prefs = {"profile.managed_default_content_settings.images": 2} - options.add_experimental_option("prefs", prefs) - - browser = webdriver.Chrome(options=options) - return browser - - -def render_html(url, tag_to_wait=None, timeout=10): - try: - browser = open_browser() - browser.get(url) - if tag_to_wait: - element = WebDriverWait(browser, timeout).until( - EC.presence_of_element_located((By.CSS_SELECTOR, tag_to_wait)) - ) - assert element - html_content = browser.page_source - return html_content - except Exception as e: - raise e - finally: - browser.close() - browser.quit() - - -def rename_key(dictionary, old_key, new_key): - if old_key in dictionary: - dictionary[new_key] = dictionary.pop(old_key) - - return dictionary - - def create_date_name_column(dates: list) -> list: date_names = [] today_dt = datetime.now().date() @@ -860,14 +762,6 @@ def create_date_name_column(dates: list) -> list: return date_names -def create_wind_description_column(df: pl.DataFrame) -> pl.DataFrame: - df = df.with_columns( - pl.col("wind_speed").apply(classify_wind_speed).alias("wind_description") - ) - - return df - - def get_day_name(days_to_add: float) -> str: today = date.today() day = today + timedelta(days=days_to_add) @@ -883,12 +777,7 @@ def create_direction_predominant_column(directions: list) -> list: def final_forecast_format(df: pl.DataFrame): if not df.is_empty(): df.sort(by=["date", "time", "spot_name"], descending=[True, True, True]) - df = df.with_columns( - pl.col("time") - .str.strptime(pl.Time, format="%H:%M", strict=False) - .alias("parsed time") - ) - datetimes = df["parsed time"] + datetimes = df["time"] _6_AM = time(hour=6, minute=0, second=0, microsecond=0) _19_PM = time(hour=19, minute=0, second=0, microsecond=0) mask = (datetimes >= _6_AM) & (datetimes <= _19_PM) @@ -896,8 +785,8 @@ def final_forecast_format(df: pl.DataFrame): common_columns = [ "date_name", - "date", - "time", + "date_friendly", + "time_friendly", "energy", "wave_period", "wind_direction", @@ -906,6 +795,9 @@ def final_forecast_format(df: pl.DataFrame): "nearest_tide", "tide", "datetime", + "date", + "time", + "time_graph", "spot_name", "wind_direction_predominant", "wave_direction_predominant", @@ -924,40 +816,6 @@ def degrees_to_direction(degrees): return directions[index] -def classify_wind_speed(speed_knots): - if speed_knots < 1: - return "Calma" - elif speed_knots <= 3: - return "Brisita suave" - elif speed_knots <= 6: - return "Brisa my débil" - elif speed_knots <= 10: - return "Brisa ligera" - elif speed_knots <= 16: - return "Brisa moderada" - elif speed_knots <= 21: - return "Brisa casi fuerte" - elif speed_knots <= 27: - return "Brisa fuerte" - elif speed_knots <= 33: - return "Viento fuerte" - elif speed_knots <= 40: - return "Viento duro" - elif speed_knots <= 47: - return "Viento muy duro" - elif speed_knots <= 55: - return "Temporal" - elif speed_knots <= 63: - return "Borrasca" - else: - return "Huracán" - - -def feet_to_meters(feet): - meters = feet * 0.3048 - return meters - - def datetime_to_frontend_str(dt: datetime) -> str: return dt.strftime(FRONT_END_DATE_FORMAT).capitalize() @@ -968,139 +826,13 @@ def construct_date_selection_list( date_selection = [] for scraped_datetime in scraped_datetime_list: if scraped_datetime >= min_value and scraped_datetime <= max_value: - date_selection.append(datetime_to_frontend_str(scraped_datetime)) + date_selection.append(scraped_datetime) return date_selection -def datetime_to_str(dt: datetime, dt_format: str) -> str: - return dt.strftime(dt_format) - - -def timestamp_to_datetime(timestamp_date: int) -> datetime: - return datetime.utcfromtimestamp(timestamp_date) - - -def add_offset_to_datetime(dt: datetime, utc_offset: int) -> datetime: - return dt + timedelta(hours=utc_offset) - - -def timestamp_to_datetimestr(timestamp_date: int, utc_offset: int) -> str: - datetime_dt = timestamp_to_datetime(timestamp_date) - datestr = datetime_to_str( - add_offset_to_datetime(datetime_dt, utc_offset), INTERNAL_DATE_STR_FORMAT - ) - timestr = datetime_to_str( - add_offset_to_datetime(datetime_dt, utc_offset), INTERNAL_TIME_STR_FORMAT - ) - return datestr, timestr - - def datestr_to_datetime(dtstr, format) -> datetime: return datetime.strptime(dtstr, format) - -def get_datename(dt: str): - dt_dt = datestr_to_datetime(dt, INTERNAL_DATE_STR_FORMAT).date() - - today = datetime.now().date() - tomorrow = today + timedelta(days=1) - day_after_tomorrow = today + timedelta(days=2) - - if dt_dt == today: - return "Today" - elif dt_dt == tomorrow: - return "Tomorrow" - elif dt_dt == day_after_tomorrow: - return "Day After Tomorrow" - else: - return "Another Day" - - -def kmh_to_knots(kmh): - return kmh / 1.852 - - -def mps_to_knots(mps): - return mps * 1.94384 - - -def convert_all_values_of_dict_to_min_length(data): - min_len = obtain_minimum_len_of_dict_values(data) - new_data = {} - for key, value in data.items(): - new_data[key] = value[:min_len] - return new_data - - -def obtain_minimum_len_of_dict_values(data: dict): - data_value_lens = [] - for _, value in data.items(): - data_value_lens.append(len(value)) - return min(data_value_lens) - - -def generate_dates(times: list) -> list: - dates = [] - date = datetime.now().date() - for index, time in enumerate(times): - if ( - index - 1 >= 0 - and datestr_to_datetime(time, "%H:%M").time() - < datestr_to_datetime(times[index - 1], "%H:%M").time() - ): - date += timedelta(days=1) - date_str = datetime.strftime(date, "%d/%m/%Y") - dates.append(date_str) - return dates - - -def are_contraries(dir1, dir2): - return CONTRARIES.get(dir1) == dir2 - - -def get_maximum_len_str(wind_direction, wave_direction): - if len(wind_direction) > len(wave_direction): - return wind_direction - else: - return wave_direction - - -def get_minimum_len_str(wind_direction, wave_direction): - if len(wind_direction) > len(wave_direction): - return wave_direction - else: - return wind_direction - - -def count_contraries(wind_direction, wave_direction): - counter = 0 - if len(wind_direction) == len(wave_direction): - for i in range(len(wind_direction)): - if are_contraries(wind_direction[i], wave_direction[i]): - counter += 1 - else: - max_len_str = get_maximum_len_str(wind_direction, wave_direction) - min_len_str = get_minimum_len_str(wind_direction, wave_direction) - for mili in min_len_str: - for mali in max_len_str: - if are_contraries(mili, mali): - counter += 1 - if counter >= len(min_len_str): - break - return counter - - -def get_wind_status(wind_direction, wave_direction): - wd_len = len(wind_direction) - wv_len = len(wave_direction) - len_contraries = count_contraries(wind_direction, wave_direction) - if wd_len == wv_len and wd_len == len_contraries: - return "Offshore" - elif len_contraries >= 1: - return "Cross-off" - return "Onshore" - - def generate_tides(tide_data: list, forecast_datetimes: list) -> list: tide_status_list = [] tides_datetimes_list = [item["datetime"] for item in tide_data] @@ -1215,116 +947,6 @@ def calculate_tide_percentage( return returned_percentage -def generate_datetimes(dates, times): - datetimes = [] - for date, time in zip(dates, times): - year = datetime.strptime(date, FRONT_END_DATE_FORMAT).year - month = datetime.strptime(date, FRONT_END_DATE_FORMAT).month - day = datetime.strptime(date, FRONT_END_DATE_FORMAT).day - hour = int(time.split(":")[0]) - minute = int(time.split(":")[1]) - datetimes.append(datetime(year, month, day, hour, minute)) - return datetimes - - -def datestr_to_frontend_format(input_text): - # si es menor es del próximo mes, si es mayor o igual es de este mes - day = re.search(r"\d+", input_text).group() - - current_date = datetime.now() - - if int(day) < current_date.day: - new_date = current_date + relativedelta(months=1) - month = new_date.month - elif int(day) >= current_date.day: - month = current_date.month - if int(month) < current_date.month: - new_date = current_date + relativedelta(years=1) - year = new_date.year - elif int(month) >= current_date.month: - year = current_date.year - date_datetime = datetime.strptime(f"{day}/{month}/{year}", "%d/%m/%Y") - - return datetime_to_frontend_str(date_datetime) - - -def filter_dataframe( - entry_df: pl.DataFrame, spot_conditions: dict, three_near_days: bool -) -> pl.DataFrame: - column_names = list(entry_df.columns) - spot_name_list = [] - wave_direction_list = [] - wind_direction_list = [] - spot_names = spot_conditions["spot_name"] - # TODO refactorizar esto - for spot in spot_names: - filtered_df = entry_df.filter(pl.col("spot_name").str.contains(spot)) - spot_name_list.append(filtered_df) - - result_list = [df for df in spot_name_list if not df.is_empty()] - if len(result_list) > 0: - result_df = result_list[0] - else: - return pl.DataFrame() - - if "wave_direction" in spot_conditions and "wave_direction" in column_names: - directions = spot_conditions["wave_direction"] - for direction in directions: - filtered_df = result_df.filter( - pl.col("wave_direction").str.contains(direction) - ) - wave_direction_list.append(filtered_df) - - result_list = [df for df in wave_direction_list if not df.is_empty()] - if len(result_list) > 0: - result_df = result_list[0] - else: - return pl.DataFrame() - - if "wave_period" in spot_conditions and "wave_period" in column_names: - result_df = result_df.filter( - pl.col("wave_period") >= spot_conditions["wave_period"] - ) - if "wave_height" in spot_conditions and "wave_height" in column_names: - result_df = result_df.filter( - pl.col("wave_height") >= spot_conditions["wave_height"] - ) - if "energy" in spot_conditions and "energy" in column_names: - result_df = result_df.filter(pl.col("energy") >= spot_conditions["energy"]) - - if "wind_direction" in spot_conditions and "wind_direction" in column_names: - directions = spot_conditions["wind_direction"] - for direction in directions: - filtered_df = result_df.filter( - pl.col("wind_direction").str.contains(direction) - ) - wind_direction_list.append(filtered_df) - - result_list = [df for df in wind_direction_list if not df.is_empty()] - if len(result_list) > 0: - result_df = result_list[0] - else: - return pl.DataFrame() - - if three_near_days: - date_names = ["Hoy", "Mañana", "Pasado"] - result_df = result_df.filter(pl.col("date_name").is_in(date_names)) - return result_df - - -def read_json(json_name: str) -> dict: - with open(json_name) as f: - return json.load(f) - - -def filter_spot_dataframe( - spot_name: str, df: pl.DataFrame, three_near_days: bool -) -> pl.DataFrame: - file = f"./assets/conditions.json" - conditions_data = read_json(file) - return filter_dataframe(df, conditions_data[spot_name], three_near_days) - - def get_predominant_direction(direction: float) -> str: if direction == 0 or direction == 360: return "N" @@ -1342,3 +964,39 @@ def get_predominant_direction(direction: float) -> str: return "SW" elif direction > 270 and direction < 360: return "NW" + + +def degrees_to_direction(degrees: int) -> str: + + compass_directions = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + ] + + sector_size = 360 / len(compass_directions) + degrees = degrees % 360 + index = int((degrees + sector_size / 2) // sector_size) % len(compass_directions) + return compass_directions[index] + + +def generate_forecast_moments(initstamp, hours): + init_datetime = datetime.fromtimestamp(initstamp, tz=timezone.utc).replace( + tzinfo=None + ) + moments = [init_datetime + timedelta(hours=hour) for hour in hours] + + return moments