nph-client 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: nph-client
3
+ Version: 0.1.0
4
+ Summary: Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA.
5
+ Author: NPH UNISANTA
6
+ Keywords: nph,nph-client,oceanografia,meteorologia,plotly,graficos,nivel-do-mar,chuva,unisanta
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pandas>=1.5.0
12
+ Requires-Dist: plotly>=5.0.0
13
+ Requires-Dist: requests>=2.25.0
14
+ Requires-Dist: python-dateutil>=2.8.0
15
+ Requires-Dist: numpy>=1.20.0
16
+ Requires-Dist: nbformat>=4.2.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
19
+ Requires-Dist: kaleido>=0.2.1; extra == "dev"
20
+
21
+ # nph-client 🌊
22
+
23
+ Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
24
+
25
+ ## Instalação
26
+
27
+ ```bash
28
+ pip install nph-client
29
+ ```
30
+
31
+ ## Principais Funções
32
+
33
+ ### 1. Nível do Mar (`plot_nivel_mar`)
34
+ Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
35
+ - **OBSERVAÇÃO**: `< 1.8m`
36
+ - **ATENÇÃO**: `1.8m a 2.0m`
37
+ - **ALERTA**: `≥ 2.0m`
38
+
39
+ ```python
40
+ from nph_client import NPHClient, plot_nivel_mar
41
+
42
+ client = NPHClient(api_key="SUA_API_KEY")
43
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
44
+
45
+ fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Estação")
46
+ fig.show()
47
+ ```
48
+
49
+ > **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, utilize `show_thresholds=False`.
50
+
51
+ ---
52
+
53
+ ### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
54
+ Plota o gráfico de barras para chuva (mm):
55
+
56
+ ```python
57
+ from nph_client import NPHClient, plot_chuva
58
+
59
+ client = NPHClient(api_key="SUA_API_KEY")
60
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
61
+
62
+ fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Estação")
63
+ fig.show()
64
+ ```
65
+
66
+ ---
67
+
68
+ ### 3. Gráfico Combinado Nível e Precipitação (`plot_combinado_nivel_precipitacao`)
69
+ Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
70
+
71
+ ```python
72
+ from nph_client import NPHClient, plot_combinado_nivel_precipitacao
73
+
74
+ client = NPHClient(api_key="SUA_API_KEY")
75
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
76
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
77
+
78
+ fig = plot_combinado_nivel_precipitacao(
79
+ df_nivel=df_nivel,
80
+ df_chuva=df_chuva,
81
+ title="Gráfico Combinado: Nível e Precipitação"
82
+ )
83
+ fig.show()
84
+ ```
85
+
86
+ ## Requisitos
87
+ - `pandas`
88
+ - `plotly`
89
+ - `requests`
90
+ - `python-dateutil`
91
+ - `numpy`
92
+ - `nbformat`
93
+
94
+ ## Licença
95
+ Licenciado sob a licença MIT.
@@ -0,0 +1,75 @@
1
+ # nph-client 🌊
2
+
3
+ Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ pip install nph-client
9
+ ```
10
+
11
+ ## Principais Funções
12
+
13
+ ### 1. Nível do Mar (`plot_nivel_mar`)
14
+ Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
15
+ - **OBSERVAÇÃO**: `< 1.8m`
16
+ - **ATENÇÃO**: `1.8m a 2.0m`
17
+ - **ALERTA**: `≥ 2.0m`
18
+
19
+ ```python
20
+ from nph_client import NPHClient, plot_nivel_mar
21
+
22
+ client = NPHClient(api_key="SUA_API_KEY")
23
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
24
+
25
+ fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Estação")
26
+ fig.show()
27
+ ```
28
+
29
+ > **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, utilize `show_thresholds=False`.
30
+
31
+ ---
32
+
33
+ ### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
34
+ Plota o gráfico de barras para chuva (mm):
35
+
36
+ ```python
37
+ from nph_client import NPHClient, plot_chuva
38
+
39
+ client = NPHClient(api_key="SUA_API_KEY")
40
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
41
+
42
+ fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Estação")
43
+ fig.show()
44
+ ```
45
+
46
+ ---
47
+
48
+ ### 3. Gráfico Combinado Nível e Precipitação (`plot_combinado_nivel_precipitacao`)
49
+ Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
50
+
51
+ ```python
52
+ from nph_client import NPHClient, plot_combinado_nivel_precipitacao
53
+
54
+ client = NPHClient(api_key="SUA_API_KEY")
55
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
56
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
57
+
58
+ fig = plot_combinado_nivel_precipitacao(
59
+ df_nivel=df_nivel,
60
+ df_chuva=df_chuva,
61
+ title="Gráfico Combinado: Nível e Precipitação"
62
+ )
63
+ fig.show()
64
+ ```
65
+
66
+ ## Requisitos
67
+ - `pandas`
68
+ - `plotly`
69
+ - `requests`
70
+ - `python-dateutil`
71
+ - `numpy`
72
+ - `nbformat`
73
+
74
+ ## Licença
75
+ Licenciado sob a licença MIT.
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nph-client"
7
+ version = "0.1.0"
8
+ description = "Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "NPH UNISANTA" }
12
+ ]
13
+ keywords = ["nph", "nph-client", "oceanografia", "meteorologia", "plotly", "graficos", "nivel-do-mar", "chuva", "unisanta"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "pandas>=1.5.0",
21
+ "plotly>=5.0.0",
22
+ "requests>=2.25.0",
23
+ "python-dateutil>=2.8.0",
24
+ "numpy>=1.20.0",
25
+ "nbformat>=4.2.0"
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0.0",
31
+ "kaleido>=0.2.1"
32
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,30 @@
1
+ from .charts import (
2
+ plot_nivel_mar,
3
+ plot_chuva,
4
+ plot_combinado_nivel_precipitacao
5
+ )
6
+ from .client import NPHClient, MODELS_GLOSSARY, PARAMS_GLOSSARY
7
+ from .thresholds import (
8
+ Threshold,
9
+ get_ppdc_sea_level_thresholds,
10
+ PPDC_THRESHOLD_ATENCAO,
11
+ PPDC_THRESHOLD_ALERTA
12
+ )
13
+ from .plotting import AxisConfig, plot_base_series
14
+
15
+ __all__ = [
16
+ "plot_nivel_mar",
17
+ "plot_chuva",
18
+ "plot_combinado_nivel_precipitacao",
19
+ "NPHClient",
20
+ "Threshold",
21
+ "get_ppdc_sea_level_thresholds",
22
+ "PPDC_THRESHOLD_ATENCAO",
23
+ "PPDC_THRESHOLD_ALERTA",
24
+ "AxisConfig",
25
+ "plot_base_series",
26
+ "MODELS_GLOSSARY",
27
+ "PARAMS_GLOSSARY"
28
+ ]
29
+
30
+ __version__ = "0.1.0"
@@ -0,0 +1,156 @@
1
+ import logging
2
+ import pandas as pd
3
+ from typing import Optional, List, Union, Tuple
4
+ import plotly.graph_objects as go
5
+
6
+ from .thresholds import Threshold, get_ppdc_sea_level_thresholds
7
+ from .plotting import AxisConfig, plot_base_series
8
+ from .client import NPHClient
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def plot_nivel_mar(
14
+ df: Optional[pd.DataFrame] = None,
15
+ stations: Optional[Union[List[Union[str, int]], str, int]] = None,
16
+ start_date: Optional[str] = None,
17
+ end_date: Optional[str] = None,
18
+ api_key: Optional[str] = None,
19
+ client: Optional[NPHClient] = None,
20
+ columns: Optional[List[str]] = None,
21
+ title: str = "Nível do Mar - PPDC Ressacas (NPH-UNISANTA)",
22
+ thresholds: Optional[List[Threshold]] = None,
23
+ show_thresholds: bool = True,
24
+ show_peaks: bool = True,
25
+ figsize: Tuple[int, int] = (12, 6),
26
+ save_path: Optional[str] = None,
27
+ auto_open: bool = False
28
+ ) -> go.Figure:
29
+ if df is None:
30
+ cli = client or NPHClient(api_key=api_key)
31
+ df = cli.get_measurements(stations=stations, start_date=start_date, end_date=end_date)
32
+ if df.empty:
33
+ df = cli.get_forecasts(stations=stations, start_date=start_date, end_date=end_date, param_id="Nível do Mar")
34
+
35
+ if df.empty:
36
+ raise ValueError("Nenhum dado encontrado para gerar o gráfico de Nível do Mar.")
37
+
38
+ if not show_thresholds:
39
+ thresholds_to_use = []
40
+ elif thresholds is not None:
41
+ thresholds_to_use = thresholds
42
+ else:
43
+ thresholds_to_use = get_ppdc_sea_level_thresholds()
44
+
45
+ primary_conf = AxisConfig(
46
+ label="Nível do Mar (m)",
47
+ columns=columns,
48
+ plot_type='line',
49
+ show_peaks=show_peaks,
50
+ thresholds=thresholds_to_use,
51
+ color_palette='Ocean'
52
+ )
53
+
54
+ return plot_base_series(
55
+ df=df,
56
+ primary_conf=primary_conf,
57
+ title=title,
58
+ figsize=figsize,
59
+ save_path=save_path,
60
+ auto_open=auto_open
61
+ )
62
+
63
+
64
+ def plot_chuva(
65
+ df: Optional[pd.DataFrame] = None,
66
+ stations: Optional[Union[List[Union[str, int]], str, int]] = None,
67
+ start_date: Optional[str] = None,
68
+ end_date: Optional[str] = None,
69
+ api_key: Optional[str] = None,
70
+ client: Optional[NPHClient] = None,
71
+ columns: Optional[List[str]] = None,
72
+ title: str = "Precipitação Pluviométrica (NPH-UNISANTA)",
73
+ figsize: Tuple[int, int] = (12, 6),
74
+ save_path: Optional[str] = None,
75
+ auto_open: bool = False
76
+ ) -> go.Figure:
77
+ if df is None:
78
+ cli = client or NPHClient(api_key=api_key)
79
+ df = cli.get_measurements(stations=stations, start_date=start_date, end_date=end_date)
80
+ if df.empty:
81
+ df = cli.get_forecasts(stations=stations, start_date=start_date, end_date=end_date, param_id="Precipitação")
82
+
83
+ if df.empty:
84
+ raise ValueError("Nenhum dado encontrado para gerar o gráfico de Chuva.")
85
+
86
+ primary_conf = AxisConfig(
87
+ label="Precipitação (mm)",
88
+ columns=columns,
89
+ plot_type='bar',
90
+ custom_colors=['#1f77b4', '#0096c7', '#03045e'],
91
+ plot_kwargs={'alpha': 0.8, 'width': 0.04}
92
+ )
93
+
94
+ return plot_base_series(
95
+ df=df,
96
+ primary_conf=primary_conf,
97
+ title=title,
98
+ figsize=figsize,
99
+ save_path=save_path,
100
+ auto_open=auto_open
101
+ )
102
+
103
+
104
+ def plot_combinado_nivel_precipitacao(
105
+ df_nivel: pd.DataFrame,
106
+ df_chuva: pd.DataFrame,
107
+ title: str = "Gráfico Combinado: Nível do Mar e Precipitação (NPH-UNISANTA)",
108
+ level_columns: Optional[List[str]] = None,
109
+ precip_columns: Optional[List[str]] = None,
110
+ thresholds: Optional[List[Threshold]] = None,
111
+ show_thresholds: bool = True,
112
+ show_peaks: bool = True,
113
+ invert_rain_axis: bool = True,
114
+ figsize: Tuple[int, int] = (12, 6),
115
+ save_path: Optional[str] = None,
116
+ auto_open: bool = False
117
+ ) -> go.Figure:
118
+ if df_nivel.empty:
119
+ raise ValueError("O DataFrame de nível do mar está vazio.")
120
+ if df_chuva.empty:
121
+ raise ValueError("O DataFrame de chuva está vazio.")
122
+
123
+ if not show_thresholds:
124
+ thresholds_to_use = []
125
+ elif thresholds is not None:
126
+ thresholds_to_use = thresholds
127
+ else:
128
+ thresholds_to_use = get_ppdc_sea_level_thresholds()
129
+
130
+ primary_conf = AxisConfig(
131
+ label="Nível do Mar (m)",
132
+ columns=level_columns,
133
+ plot_type='line',
134
+ show_peaks=show_peaks,
135
+ thresholds=thresholds_to_use,
136
+ color_palette='Ocean'
137
+ )
138
+
139
+ secondary_conf = AxisConfig(
140
+ label="Precipitação (mm)",
141
+ columns=precip_columns,
142
+ plot_type='bar',
143
+ invert_yaxis=invert_rain_axis,
144
+ custom_colors=['#2b5c8f', '#4682b4'],
145
+ plot_kwargs={'alpha': 0.65, 'width': 0.04}
146
+ )
147
+
148
+ return plot_base_series(
149
+ df=[df_nivel, df_chuva],
150
+ primary_conf=primary_conf,
151
+ secondary_conf=secondary_conf,
152
+ title=title,
153
+ figsize=figsize,
154
+ save_path=save_path,
155
+ auto_open=auto_open
156
+ )
@@ -0,0 +1,257 @@
1
+ import logging
2
+ import requests
3
+ import pandas as pd
4
+ import time
5
+ from datetime import date, datetime, timedelta
6
+ from dateutil.relativedelta import relativedelta
7
+ from typing import List, Dict, Union, Optional, Any
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ _STATION_COLUMNS = [
12
+ "id", "station_prefix_id", "station_prefix", "station_name",
13
+ "latitude", "longitude", "station_type", "city_name",
14
+ "station_owner", "station_operator", "ugrhi_name",
15
+ "subugrhi_name", "station_status"
16
+ ]
17
+
18
+ _STATION_RENAME_MAP = {
19
+ "station_prefix_id": "id_sibh",
20
+ "station_prefix": "prefixo",
21
+ "station_name": "nome_da_estacao",
22
+ "latitude": "latitude",
23
+ "longitude": "longitude",
24
+ "station_type": "tipo_da_estacao",
25
+ "city_name": "cidade",
26
+ "station_owner": "proprietario",
27
+ "station_operator": "operador",
28
+ "ugrhi_name": "ugrhi",
29
+ "sub_ugrhi_name": "sub_ugrhi",
30
+ "station_status": "status"
31
+ }
32
+
33
+ MODELS_GLOSSARY = {
34
+ "WRF 3km": 1,
35
+ "WRF 1km": 2,
36
+ "ECMWF 9km": 3,
37
+ "WRF 7km": 13,
38
+ "GFS 25km": 15,
39
+ "MOHID": 4,
40
+ "Delft - FLOW (BAIXADA)": 5,
41
+ "Delft - WAVE (BAIXADA)": 6,
42
+ "Delft - FLOW (SESSV)": 7,
43
+ "Delft - WAVE (SESSV)": 8,
44
+ "SWAN v1": 9,
45
+ "SWAN v2": 10,
46
+ "CMS - Hydro Hourly": 11,
47
+ "CMS - Wave": 12,
48
+ "NALA 10min": 17,
49
+ "NALA 1h": 18,
50
+ "NALA 3h": 19,
51
+ "NALA Best": 20
52
+ }
53
+
54
+ PARAMS_GLOSSARY = {
55
+ "Temperatura": 1,
56
+ "Precipitação": 2,
57
+ "Pressão": 3,
58
+ "Rajada de Vento": 4,
59
+ "Velocidade do Vento": 5,
60
+ "Direção do Vento": 6,
61
+ "CAPE": 7,
62
+ "LIFT": 8,
63
+ "Umidade Relativa": 9,
64
+ "Cobertura Total de Nuvens": 10,
65
+ "Ponto de Orvalho": 11,
66
+ "Precipitação Convectiva": 12,
67
+ "Nível do Mar": 13,
68
+ "Direção da Corrente": 14,
69
+ "Velocidade da Corrente": 15,
70
+ "Altura Significativa das Ondas (Hs)": 16,
71
+ "Direção média das ondas": 17,
72
+ "Período de Pico (Tp)": 18,
73
+ "Direção de Pico das Ondas": 19
74
+ }
75
+
76
+
77
+ class NPHClient:
78
+
79
+ def __init__(self, api_key: Optional[str] = None):
80
+ self.api_key = api_key
81
+ self.base_url = 'https://nph.unisanta.br/ssbs/api'
82
+
83
+ def _get_headers(self) -> Dict[str, str]:
84
+ return {
85
+ 'apiKey': self.api_key or '',
86
+ 'Content-Type': 'application/json'
87
+ }
88
+
89
+ @staticmethod
90
+ def _resolve_value_col(status: int) -> str:
91
+ if status == 0:
92
+ return 'primary_value'
93
+ elif status == 1:
94
+ return 'secondary_value'
95
+ elif status == 2:
96
+ return 'extra_values'
97
+ else:
98
+ return 'primary_value'
99
+
100
+ def _fetch_paginated_data(
101
+ self,
102
+ url: str,
103
+ start_dt: Optional[Union[datetime, bool]] = None,
104
+ end_dt: Optional[Union[datetime, bool]] = None,
105
+ base_params: Optional[Dict[str, Any]] = None
106
+ ) -> pd.DataFrame:
107
+ base_params = base_params or {}
108
+
109
+ if not start_dt or not end_dt:
110
+ try:
111
+ response = requests.get(url=url, headers=self._get_headers(), params=base_params)
112
+ response.raise_for_status()
113
+ data = response.json()
114
+ return pd.DataFrame(data) if data else pd.DataFrame()
115
+ except Exception as e:
116
+ logger.error(f"Erro ao buscar dados: {e}")
117
+ return pd.DataFrame()
118
+
119
+ all_dataframes = []
120
+ current_start = start_dt
121
+
122
+ while current_start < end_dt:
123
+ chunk_end = min(current_start + relativedelta(years=1), end_dt)
124
+ params = base_params.copy()
125
+ params['startDate'] = current_start.strftime('%Y-%m-%d %H:%M:%S')
126
+ params['endDate'] = chunk_end.strftime('%Y-%m-%d %H:%M:%S')
127
+
128
+ try:
129
+ response = requests.get(url=url, headers=self._get_headers(), params=params)
130
+ response.raise_for_status()
131
+ data = response.json()
132
+ if data:
133
+ all_dataframes.append(pd.DataFrame(data))
134
+ except Exception as e:
135
+ logger.error(f"Erro ao buscar intervalo {params['startDate']}: {e}")
136
+ break
137
+
138
+ current_start = chunk_end
139
+ if current_start < end_dt:
140
+ time.sleep(0.05)
141
+
142
+ if not all_dataframes:
143
+ return pd.DataFrame()
144
+
145
+ return pd.concat(all_dataframes, ignore_index=True)
146
+
147
+ def get_stations(self) -> pd.DataFrame:
148
+ url = f"{self.base_url}/stations"
149
+ try:
150
+ response = requests.get(url=url, headers=self._get_headers())
151
+ response.raise_for_status()
152
+ data = response.json()
153
+ if not data:
154
+ return pd.DataFrame()
155
+
156
+ df = pd.DataFrame(data)
157
+ cols_to_keep = [c for c in _STATION_COLUMNS if c in df.columns]
158
+ return df[cols_to_keep].set_index('id').rename(columns=_STATION_RENAME_MAP)
159
+ except Exception as e:
160
+ logger.error(f"Erro ao buscar estações: {e}")
161
+ return pd.DataFrame()
162
+
163
+ def get_measurements(
164
+ self,
165
+ stations: Optional[Union[List[Union[str, int]], str, int]] = None,
166
+ start_date: Optional[Union[str, date]] = None,
167
+ end_date: Optional[Union[str, datetime]] = None,
168
+ return_value: int = 0,
169
+ time_offset: Optional[timedelta] = timedelta(hours=-3)
170
+ ) -> pd.DataFrame:
171
+ start_dt = pd.to_datetime(start_date or (date.today() - timedelta(days=7)))
172
+ end_dt = pd.to_datetime(end_date or datetime.now())
173
+
174
+ base_params = {}
175
+ if stations:
176
+ if isinstance(stations, (str, int)):
177
+ base_params['stationId'] = str(stations)
178
+ elif isinstance(stations, list):
179
+ base_params['stationId'] = ','.join(map(str, stations))
180
+
181
+ df = self._fetch_paginated_data(
182
+ url=f"{self.base_url}/measurements",
183
+ start_dt=start_dt,
184
+ end_dt=end_dt,
185
+ base_params=base_params
186
+ )
187
+
188
+ if df.empty:
189
+ return df
190
+
191
+ try:
192
+ df['datetime'] = pd.to_datetime(df['datetime'])
193
+ value_col = self._resolve_value_col(return_value)
194
+
195
+ if time_offset:
196
+ df['datetime'] = df['datetime'] + time_offset
197
+
198
+ col_name = 'station_name' if 'station_name' in df.columns else 'station_id'
199
+ df_pivot = df.pivot_table(index='datetime', columns=col_name, values=value_col, aggfunc='mean')
200
+ df_pivot = df_pivot.sort_index()
201
+ return df_pivot
202
+ except Exception as e:
203
+ logger.error(f"Erro ao processar medições: {e}")
204
+ return pd.DataFrame()
205
+
206
+ def get_forecasts(
207
+ self,
208
+ stations: Optional[Union[List[Union[str, int]], str, int]] = None,
209
+ start_date: Optional[Union[str, date]] = None,
210
+ end_date: Optional[Union[str, datetime]] = None,
211
+ model_id: Union[int, str] = 1,
212
+ param_id: Union[int, str] = "Nível do Mar",
213
+ time_offset: Optional[timedelta] = timedelta(hours=-3)
214
+ ) -> pd.DataFrame:
215
+ start_dt = pd.to_datetime(start_date or (date.today() - timedelta(days=7)))
216
+ end_dt = pd.to_datetime(end_date or (datetime.now() + timedelta(days=3)))
217
+
218
+ if isinstance(model_id, str):
219
+ model_id = MODELS_GLOSSARY.get(model_id, 1)
220
+
221
+ if isinstance(param_id, str):
222
+ param_id = PARAMS_GLOSSARY.get(param_id, 13)
223
+
224
+ base_params = {
225
+ 'modelId': str(model_id),
226
+ 'parameterId': str(param_id)
227
+ }
228
+
229
+ if stations:
230
+ if isinstance(stations, (str, int)):
231
+ base_params['stationId'] = str(stations)
232
+ elif isinstance(stations, list):
233
+ base_params['stationId'] = ','.join(map(str, stations))
234
+
235
+ df = self._fetch_paginated_data(
236
+ url=f"{self.base_url}/forecasts",
237
+ start_dt=start_dt,
238
+ end_dt=end_dt,
239
+ base_params=base_params
240
+ )
241
+
242
+ if df.empty:
243
+ return df
244
+
245
+ try:
246
+ df['datetime'] = pd.to_datetime(df['datetime'])
247
+ if time_offset:
248
+ df['datetime'] = df['datetime'] + time_offset
249
+
250
+ col_name = 'station_name' if 'station_name' in df.columns else 'station_id'
251
+ value_col = 'value' if 'value' in df.columns else df.columns[-1]
252
+ df_pivot = df.pivot_table(index='datetime', columns=col_name, values=value_col, aggfunc='mean')
253
+ df_pivot = df_pivot.sort_index()
254
+ return df_pivot
255
+ except Exception as e:
256
+ logger.error(f"Erro ao processar previsões: {e}")
257
+ return pd.DataFrame()
@@ -0,0 +1,310 @@
1
+ import logging
2
+ import pandas as pd
3
+ import numpy as np
4
+ from typing import List, Optional, Tuple, Dict, Any, Literal, Union
5
+ from dataclasses import dataclass, field
6
+ import plotly.graph_objects as go
7
+ from plotly.subplots import make_subplots
8
+ import plotly.colors as pcolors
9
+
10
+ from .thresholds import Threshold
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ LINESTYLE_MAP = {
15
+ '-': 'solid',
16
+ '--': 'dash',
17
+ ':': 'dot',
18
+ '-.': 'dashdot'
19
+ }
20
+
21
+ PALETTE_MAP = {
22
+ 'tab10': pcolors.qualitative.T10,
23
+ 'Set1': pcolors.qualitative.Set1,
24
+ 'Dark2': pcolors.qualitative.Dark2,
25
+ 'Plotly': pcolors.qualitative.Plotly,
26
+ 'Ocean': ['#0077b6', '#00b4d8', '#90e0ef', '#03045e', '#0096c7']
27
+ }
28
+
29
+
30
+ @dataclass
31
+ class AxisConfig:
32
+ label: str = "Valor"
33
+ columns: Optional[List[str]] = None
34
+ plot_type: Literal['line', 'scatter', 'bar', 'area'] = 'line'
35
+ min_val: Optional[float] = None
36
+ max_val: Optional[float] = None
37
+ step: Optional[float] = None
38
+ show_peaks: bool = False
39
+ color_palette: str = 'tab10'
40
+ invert_yaxis: bool = False
41
+ custom_colors: Optional[List[str]] = None
42
+ plot_kwargs: Dict[str, Any] = field(default_factory=dict)
43
+ thresholds: List[Threshold] = field(default_factory=list)
44
+
45
+
46
+ def get_palette_colors(palette_name: str) -> List[str]:
47
+ return PALETTE_MAP.get(palette_name, pcolors.qualitative.T10)
48
+
49
+
50
+ def hex_to_rgba(hex_str: str, alpha: float) -> str:
51
+ try:
52
+ hex_str = hex_str.lstrip('#')
53
+ if len(hex_str) == 3:
54
+ hex_str = ''.join([c * 2 for c in hex_str])
55
+ r, g, b = (int(hex_str[i:i+2], 16) for i in (0, 2, 4))
56
+ return f"rgba({r}, {g}, {b}, {alpha})"
57
+ except Exception:
58
+ return f"rgba(31, 119, 180, {alpha})"
59
+
60
+
61
+ def annotate_peaks(fig: go.Figure, series: pd.Series, color: str, secondary_y: bool):
62
+ clean_series = series.dropna()
63
+ if clean_series.empty:
64
+ return
65
+
66
+ max_val = clean_series.max()
67
+ max_date = clean_series.idxmax()
68
+ max_date_str = max_date.isoformat() if hasattr(max_date, 'isoformat') else str(max_date)
69
+
70
+ min_val = clean_series.min()
71
+ min_date = clean_series.idxmin()
72
+ min_date_str = min_date.isoformat() if hasattr(min_date, 'isoformat') else str(min_date)
73
+
74
+ fig.add_trace(
75
+ go.Scatter(
76
+ x=[max_date_str],
77
+ y=[max_val],
78
+ mode='markers+text',
79
+ marker=dict(color=color, size=10, line=dict(color='black', width=1.5)),
80
+ text=[f"Máx: {max_val:.2f}"],
81
+ textposition="top center",
82
+ textfont=dict(color='black', size=11, family="Arial"),
83
+ showlegend=False,
84
+ hoverinfo='skip'
85
+ ),
86
+ secondary_y=secondary_y
87
+ )
88
+
89
+ if min_date != max_date:
90
+ fig.add_trace(
91
+ go.Scatter(
92
+ x=[min_date_str],
93
+ y=[min_val],
94
+ mode='markers+text',
95
+ marker=dict(color=color, size=9, line=dict(color='black', width=1)),
96
+ text=[f"Mín: {min_val:.2f}"],
97
+ textposition="bottom center",
98
+ textfont=dict(color='#555555', size=10, family="Arial"),
99
+ showlegend=False,
100
+ hoverinfo='skip'
101
+ ),
102
+ secondary_y=secondary_y
103
+ )
104
+
105
+
106
+ def draw_thresholds(fig: go.Figure, config: AxisConfig, secondary_y: bool, y_range: Tuple[float, float]):
107
+ yref = 'y2' if secondary_y else 'y'
108
+ y_min, y_max = y_range
109
+
110
+ for t in config.thresholds:
111
+ dash_style = LINESTYLE_MAP.get(t.linestyle, 'dash')
112
+
113
+ fig.add_hline(
114
+ y=t.value,
115
+ line=dict(color=t.color, width=1.8, dash=dash_style),
116
+ annotation_text=f" <b>{t.label}</b> ({t.value:.2f}m)",
117
+ annotation_position="top left",
118
+ annotation_font=dict(color=t.color, size=11, family="Arial"),
119
+ yref=yref
120
+ )
121
+
122
+ y0 = t.value if t.fill_above else y_min
123
+ y1 = (t.max_fill_value if t.max_fill_value is not None else y_max) if t.fill_above else t.value
124
+
125
+ fig.add_hrect(
126
+ y0=y0,
127
+ y1=y1,
128
+ fillcolor=t.color,
129
+ opacity=t.alpha,
130
+ layer="below",
131
+ line_width=0,
132
+ yref=yref
133
+ )
134
+
135
+
136
+ def add_traces(fig: go.Figure, df: pd.DataFrame, config: AxisConfig, secondary_y: bool):
137
+ cols_to_plot = config.columns if config.columns else df.columns
138
+ colors = config.custom_colors or get_palette_colors(config.color_palette)
139
+
140
+ for i, col in enumerate(cols_to_plot):
141
+ if col not in df.columns:
142
+ logger.warning(f"Coluna '{col}' não encontrada no DataFrame.")
143
+ continue
144
+
145
+ color = colors[i % len(colors)]
146
+ clean_kwargs = {k: v for k, v in config.plot_kwargs.items() if k not in ['alpha', 'width', 'dash', 'size']}
147
+
148
+ hovertemplate = (
149
+ f"<b>{col}</b><br>"
150
+ "<b>Data/Hora:</b> %{x|%d/%m/%Y %H:%M}<br>"
151
+ f"<b>{config.label}:</b> %{{y:.2f}}<extra></extra>"
152
+ )
153
+
154
+ if config.plot_type == 'bar':
155
+ opacity = config.plot_kwargs.get('alpha', 0.75)
156
+ bar_width = None
157
+ if 'width' in config.plot_kwargs:
158
+ w = config.plot_kwargs['width']
159
+ bar_width = w * 86400000 if isinstance(df.index, pd.DatetimeIndex) else w
160
+
161
+ trace = go.Bar(
162
+ x=df.index,
163
+ y=df[col],
164
+ name=f"{col} (Chuva)",
165
+ marker_color=color,
166
+ opacity=opacity,
167
+ width=bar_width,
168
+ hovertemplate=hovertemplate,
169
+ **clean_kwargs
170
+ )
171
+ elif config.plot_type == 'scatter':
172
+ marker_size = config.plot_kwargs.get('size', 8)
173
+ trace = go.Scatter(
174
+ x=df.index,
175
+ y=df[col],
176
+ name=col,
177
+ mode='markers',
178
+ marker=dict(color=color, size=marker_size),
179
+ hovertemplate=hovertemplate,
180
+ **clean_kwargs
181
+ )
182
+ else:
183
+ line_dict = dict(color=color, width=2.5)
184
+ if 'dash' in config.plot_kwargs:
185
+ line_dict['dash'] = config.plot_kwargs['dash']
186
+ trace = go.Scatter(
187
+ x=df.index,
188
+ y=df[col],
189
+ name=col,
190
+ mode='lines',
191
+ line=line_dict,
192
+ hovertemplate=hovertemplate,
193
+ **clean_kwargs
194
+ )
195
+
196
+ fig.add_trace(trace, secondary_y=secondary_y)
197
+
198
+ if config.show_peaks and config.plot_type != 'bar':
199
+ annotate_peaks(fig, df[col], color, secondary_y)
200
+
201
+
202
+ def plot_base_series(
203
+ df: Union[pd.DataFrame, List[pd.DataFrame]],
204
+ primary_conf: AxisConfig,
205
+ secondary_conf: Optional[AxisConfig] = None,
206
+ title: str = "Série Temporal NPH",
207
+ figsize: Tuple[int, int] = (12, 6),
208
+ save_path: Optional[str] = None,
209
+ auto_open: bool = False
210
+ ) -> go.Figure:
211
+ fig = make_subplots(specs=[[{"secondary_y": True}]])
212
+
213
+ if isinstance(df, list):
214
+ df_primary = df[0]
215
+ df_secondary = df[1] if len(df) > 1 else df[0]
216
+ else:
217
+ df_primary = df
218
+ df_secondary = df
219
+
220
+ add_traces(fig, df_primary, primary_conf, secondary_y=False)
221
+
222
+ if secondary_conf:
223
+ add_traces(fig, df_secondary, secondary_conf, secondary_y=True)
224
+
225
+ primary_cols = [c for c in (primary_conf.columns or df_primary.columns) if c in df_primary.columns]
226
+
227
+ if primary_cols and not df_primary[primary_cols].empty:
228
+ p_min = df_primary[primary_cols].min().min()
229
+ p_max = df_primary[primary_cols].max().max()
230
+ p_min, p_max = (0.0, 1.0) if pd.isna(p_min) or pd.isna(p_max) else (p_min, p_max)
231
+ else:
232
+ p_min, p_max = 0.0, 1.0
233
+
234
+ all_p_vals = [p_min, p_max] + [t.value for t in primary_conf.thresholds]
235
+ y1_min_val, y1_max_val = min(all_p_vals), max(all_p_vals)
236
+ pad1 = (y1_max_val - y1_min_val) * 0.15 if y1_max_val != y1_min_val else 1.0
237
+
238
+ y1_min = primary_conf.min_val if primary_conf.min_val is not None else y1_min_val - pad1
239
+ y1_max = primary_conf.max_val if primary_conf.max_val is not None else y1_max_val + pad1
240
+ y1_range = (y1_min, y1_max)
241
+
242
+ y1_layout = dict(
243
+ title=dict(text=primary_conf.label, font=dict(family="Arial", size=13, color="#111111")),
244
+ gridcolor="rgba(200, 200, 200, 0.25)",
245
+ showgrid=True,
246
+ range=[y1_max, y1_min] if primary_conf.invert_yaxis else [y1_min, y1_max]
247
+ )
248
+
249
+ y2_range = (0.0, 10.0)
250
+ y2_layout = dict(showgrid=False)
251
+
252
+ if secondary_conf:
253
+ scols = [c for c in (secondary_conf.columns or df_secondary.columns) if c in df_secondary.columns]
254
+ if scols and not df_secondary[scols].empty:
255
+ s_min = df_secondary[scols].min().min()
256
+ s_max = df_secondary[scols].max().max()
257
+ s_min, s_max = (0.0, 10.0) if pd.isna(s_min) or pd.isna(s_max) else (s_min, s_max)
258
+ else:
259
+ s_min, s_max = 0.0, 10.0
260
+
261
+ y2_min = secondary_conf.min_val if secondary_conf.min_val is not None else 0.0
262
+ y2_max = secondary_conf.max_val if secondary_conf.max_val is not None else max(s_max * 2.5, 10.0)
263
+ y2_range = (y2_min, y2_max)
264
+
265
+ y2_layout.update(dict(
266
+ title=dict(text=secondary_conf.label, font=dict(family="Arial", size=13, color="#111111")),
267
+ range=[y2_max, y2_min] if secondary_conf.invert_yaxis else [y2_min, y2_max]
268
+ ))
269
+
270
+ if primary_conf.thresholds:
271
+ draw_thresholds(fig, primary_conf, secondary_y=False, y_range=y1_range)
272
+ if secondary_conf and secondary_conf.thresholds:
273
+ draw_thresholds(fig, secondary_conf, secondary_y=True, y_range=y2_range)
274
+
275
+ fig.update_layout(
276
+ title=dict(text=f"<b>{title}</b>", x=0.5, xanchor='center', font=dict(family="Arial", size=16)),
277
+ xaxis=dict(
278
+ title=dict(text="Data / Hora", font=dict(family="Arial", size=12)),
279
+ gridcolor="rgba(200, 200, 200, 0.25)",
280
+ showgrid=True
281
+ ),
282
+ yaxis=y1_layout,
283
+ yaxis2=y2_layout,
284
+ width=figsize[0] * 80,
285
+ height=figsize[1] * 80,
286
+ plot_bgcolor='#ffffff',
287
+ paper_bgcolor='#ffffff',
288
+ showlegend=True,
289
+ legend=dict(
290
+ orientation="h",
291
+ yanchor="bottom",
292
+ y=1.02,
293
+ xanchor="center",
294
+ x=0.5
295
+ ),
296
+ hovermode="x unified",
297
+ margin=dict(l=60, r=60, t=90, b=60)
298
+ )
299
+
300
+ if save_path:
301
+ try:
302
+ if save_path.endswith('.html'):
303
+ fig.write_html(save_path, auto_open=auto_open)
304
+ else:
305
+ fig.write_image(save_path)
306
+ logger.info(f"Gráfico salvo em {save_path}")
307
+ except Exception as e:
308
+ logger.error(f"Erro ao salvar gráfico em {save_path}: {e}")
309
+
310
+ return fig
@@ -0,0 +1,36 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional
3
+
4
+ @dataclass
5
+ class Threshold:
6
+ value: float
7
+ color: str
8
+ label: str
9
+ fill_above: bool = True
10
+ max_fill_value: Optional[float] = None
11
+ alpha: float = 0.12
12
+ linestyle: str = '--'
13
+
14
+
15
+ PPDC_THRESHOLD_ATENCAO = Threshold(
16
+ value=1.8,
17
+ color="#f39c12",
18
+ label="ATENÇÃO",
19
+ fill_above=True,
20
+ max_fill_value=2.0,
21
+ alpha=0.15,
22
+ linestyle="--"
23
+ )
24
+
25
+ PPDC_THRESHOLD_ALERTA = Threshold(
26
+ value=2.0,
27
+ color="#e74c3c",
28
+ label="ALERTA",
29
+ fill_above=True,
30
+ max_fill_value=None,
31
+ alpha=0.20,
32
+ linestyle="--"
33
+ )
34
+
35
+ def get_ppdc_sea_level_thresholds() -> List[Threshold]:
36
+ return [PPDC_THRESHOLD_ATENCAO, PPDC_THRESHOLD_ALERTA]
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: nph-client
3
+ Version: 0.1.0
4
+ Summary: Biblioteca de visualização de dados oceanográficos e meteorológicos do NPH-UNISANTA.
5
+ Author: NPH UNISANTA
6
+ Keywords: nph,nph-client,oceanografia,meteorologia,plotly,graficos,nivel-do-mar,chuva,unisanta
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pandas>=1.5.0
12
+ Requires-Dist: plotly>=5.0.0
13
+ Requires-Dist: requests>=2.25.0
14
+ Requires-Dist: python-dateutil>=2.8.0
15
+ Requires-Dist: numpy>=1.20.0
16
+ Requires-Dist: nbformat>=4.2.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
19
+ Requires-Dist: kaleido>=0.2.1; extra == "dev"
20
+
21
+ # nph-client 🌊
22
+
23
+ Biblioteca em Python para geração padronizada e simplificada de gráficos oceanográficos e meteorológicos do **NPH-UNISANTA** (Núcleo de Pesquisas Hidrodinâmicas da Unisanta).
24
+
25
+ ## Instalação
26
+
27
+ ```bash
28
+ pip install nph-client
29
+ ```
30
+
31
+ ## Principais Funções
32
+
33
+ ### 1. Nível do Mar (`plot_nivel_mar`)
34
+ Plota o gráfico de linha de nível do mar com picos destacados e limiares do **PPDC Ressacas**:
35
+ - **OBSERVAÇÃO**: `< 1.8m`
36
+ - **ATENÇÃO**: `1.8m a 2.0m`
37
+ - **ALERTA**: `≥ 2.0m`
38
+
39
+ ```python
40
+ from nph_client import NPHClient, plot_nivel_mar
41
+
42
+ client = NPHClient(api_key="SUA_API_KEY")
43
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
44
+
45
+ fig = plot_nivel_mar(df=df_nivel, title="Nível do Mar - Estação")
46
+ fig.show()
47
+ ```
48
+
49
+ > **Dica**: Para ocultar as linhas e faixas dos limiares PPDC, utilize `show_thresholds=False`.
50
+
51
+ ---
52
+
53
+ ### 2. Precipitação Pluviométrica / Chuva (`plot_chuva`)
54
+ Plota o gráfico de barras para chuva (mm):
55
+
56
+ ```python
57
+ from nph_client import NPHClient, plot_chuva
58
+
59
+ client = NPHClient(api_key="SUA_API_KEY")
60
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
61
+
62
+ fig = plot_chuva(df=df_chuva, title="Precipitação Pluviométrica - Estação")
63
+ fig.show()
64
+ ```
65
+
66
+ ---
67
+
68
+ ### 3. Gráfico Combinado Nível e Precipitação (`plot_combinado_nivel_precipitacao`)
69
+ Reúne Nível do Mar e Chuva no mesmo gráfico com eixo secundário:
70
+
71
+ ```python
72
+ from nph_client import NPHClient, plot_combinado_nivel_precipitacao
73
+
74
+ client = NPHClient(api_key="SUA_API_KEY")
75
+ df_nivel = client.get_measurements(stations="ESTACAO_NIVEL", start_date="2026-08-05", end_date="2026-08-07")
76
+ df_chuva = client.get_measurements(stations="ESTACAO_CHUVA", start_date="2026-08-05", end_date="2026-08-07")
77
+
78
+ fig = plot_combinado_nivel_precipitacao(
79
+ df_nivel=df_nivel,
80
+ df_chuva=df_chuva,
81
+ title="Gráfico Combinado: Nível e Precipitação"
82
+ )
83
+ fig.show()
84
+ ```
85
+
86
+ ## Requisitos
87
+ - `pandas`
88
+ - `plotly`
89
+ - `requests`
90
+ - `python-dateutil`
91
+ - `numpy`
92
+ - `nbformat`
93
+
94
+ ## Licença
95
+ Licenciado sob a licença MIT.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/nph_client/__init__.py
4
+ src/nph_client/charts.py
5
+ src/nph_client/client.py
6
+ src/nph_client/plotting.py
7
+ src/nph_client/thresholds.py
8
+ src/nph_client.egg-info/PKG-INFO
9
+ src/nph_client.egg-info/SOURCES.txt
10
+ src/nph_client.egg-info/dependency_links.txt
11
+ src/nph_client.egg-info/requires.txt
12
+ src/nph_client.egg-info/top_level.txt
13
+ tests/test_graficos.py
@@ -0,0 +1,10 @@
1
+ pandas>=1.5.0
2
+ plotly>=5.0.0
3
+ requests>=2.25.0
4
+ python-dateutil>=2.8.0
5
+ numpy>=1.20.0
6
+ nbformat>=4.2.0
7
+
8
+ [dev]
9
+ pytest>=7.0.0
10
+ kaleido>=0.2.1
@@ -0,0 +1 @@
1
+ nph_client
@@ -0,0 +1,64 @@
1
+ import unittest
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+
6
+ from nph_client import (
7
+ plot_nivel_mar,
8
+ plot_chuva,
9
+ plot_combinado_nivel_precipitacao,
10
+ get_ppdc_sea_level_thresholds,
11
+ NPHClient
12
+ )
13
+
14
+
15
+ class TestNPHClient(unittest.TestCase):
16
+
17
+ def setUp(self):
18
+ dates = pd.date_range(start="2026-08-01", periods=48, freq="h")
19
+ np.random.seed(42)
20
+
21
+ nivel = 1.6 + 0.5 * np.sin(np.linspace(0, 4 * np.pi, 48)) + np.random.normal(0, 0.1, 48)
22
+ self.df_nivel = pd.DataFrame({"Praticagem de Santos": nivel}, index=dates)
23
+
24
+ chuva = np.random.exponential(scale=2.0, size=48)
25
+ self.df_chuva = pd.DataFrame({"Portão 40": chuva}, index=dates)
26
+
27
+ def test_plot_nivel_mar(self):
28
+ fig = plot_nivel_mar(self.df_nivel, title="Nível do Mar")
29
+ self.assertIsInstance(fig, go.Figure)
30
+ self.assertEqual(fig.layout.title.text, "<b>Nível do Mar</b>")
31
+
32
+ def test_plot_nivel_mar_opcoes(self):
33
+ fig = plot_nivel_mar(self.df_nivel, show_thresholds=False, show_peaks=False)
34
+ self.assertIsInstance(fig, go.Figure)
35
+
36
+ def test_plot_chuva(self):
37
+ fig = plot_chuva(self.df_chuva, title="Precipitação")
38
+ self.assertIsInstance(fig, go.Figure)
39
+ self.assertEqual(fig.layout.title.text, "<b>Precipitação</b>")
40
+
41
+ def test_plot_combinado_nivel_precipitacao(self):
42
+ fig = plot_combinado_nivel_precipitacao(self.df_nivel, self.df_chuva, title="Gráfico Combinado")
43
+ self.assertIsInstance(fig, go.Figure)
44
+ self.assertEqual(fig.layout.title.text, "<b>Gráfico Combinado</b>")
45
+
46
+ def test_plot_combinado_nivel_precipitacao_sem_limiares(self):
47
+ fig = plot_combinado_nivel_precipitacao(self.df_nivel, self.df_chuva, show_thresholds=False)
48
+ self.assertIsInstance(fig, go.Figure)
49
+
50
+ def test_client_init(self):
51
+ client = NPHClient(api_key="teste_key")
52
+ self.assertEqual(client.api_key, "teste_key")
53
+
54
+ def test_ppdc_thresholds(self):
55
+ thresholds = get_ppdc_sea_level_thresholds()
56
+ self.assertEqual(len(thresholds), 2)
57
+ self.assertEqual(thresholds[0].value, 1.8)
58
+ self.assertEqual(thresholds[0].label, "ATENÇÃO")
59
+ self.assertEqual(thresholds[1].value, 2.0)
60
+ self.assertEqual(thresholds[1].label, "ALERTA")
61
+
62
+
63
+ if __name__ == "__main__":
64
+ unittest.main()