meteocat 2.2.6 → 2.3.0
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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/workflows/autocloser.yaml +25 -0
- package/.github/workflows/close-duplicates.yml +57 -0
- package/.github/workflows/publish-zip.yml +67 -0
- package/.github/workflows/release.yml +38 -6
- package/.github/workflows/stale.yml +12 -0
- package/.github/workflows/sync-gitlab.yml +94 -0
- package/.releaserc +1 -8
- package/CHANGELOG.md +29 -0
- package/README.md +29 -4
- package/custom_components/meteocat/__init__.py +154 -110
- package/custom_components/meteocat/config_flow.py +125 -55
- package/custom_components/meteocat/coordinator.py +200 -368
- package/custom_components/meteocat/helpers.py +12 -0
- package/custom_components/meteocat/manifest.json +22 -11
- package/custom_components/meteocat/options_flow.py +46 -2
- package/custom_components/meteocat/sensor.py +47 -8
- package/custom_components/meteocat/strings.json +10 -2
- package/custom_components/meteocat/translations/ca.json +10 -2
- package/custom_components/meteocat/translations/en.json +10 -2
- package/custom_components/meteocat/translations/es.json +10 -2
- package/custom_components/meteocat/version.py +1 -2
- package/filetree.txt +9 -0
- package/hacs.json +5 -2
- package/images/options.png +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/.releaserc.toml +0 -14
- package/releaserc.json +0 -18
|
@@ -2,11 +2,23 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from homeassistant.core import HomeAssistant
|
|
5
7
|
from homeassistant.util.dt import as_local, as_utc, start_of_local_day
|
|
6
8
|
from homeassistant.helpers.sun import get_astral_event_date
|
|
7
9
|
|
|
8
10
|
_LOGGER = logging.getLogger(__name__)
|
|
9
11
|
|
|
12
|
+
# Ruta base para guardar archivos persistentes que se descargan de la API y que son utilizados por los coordinadores
|
|
13
|
+
def get_storage_dir(hass: HomeAssistant, subdir: str | None = None) -> Path:
|
|
14
|
+
"""Devuelve una ruta persistente en config/meteocat_files[/subdir]."""
|
|
15
|
+
base_dir = Path(hass.config.path("meteocat_files"))
|
|
16
|
+
if subdir:
|
|
17
|
+
base_dir = base_dir / subdir
|
|
18
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
return base_dir
|
|
20
|
+
|
|
21
|
+
# Cálculo de amanecer y atardecer para definir cuando es de noche
|
|
10
22
|
def get_sun_times(hass, current_time=None):
|
|
11
23
|
"""Obtén las horas de amanecer y atardecer para el día actual."""
|
|
12
24
|
if current_time is None:
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"domain": "meteocat",
|
|
3
|
+
"name": "Meteocat",
|
|
4
|
+
"codeowners": [
|
|
5
|
+
"@figorr"
|
|
6
|
+
],
|
|
7
|
+
"config_flow": true,
|
|
8
|
+
"dependencies": [
|
|
9
|
+
"persistent_notification",
|
|
10
|
+
"http"
|
|
11
|
+
],
|
|
12
|
+
"documentation": "https://github.com/figorr/meteocat",
|
|
13
|
+
"iot_class": "cloud_polling",
|
|
14
|
+
"issue_tracker": "https://github.com/figorr/meteocat/issues",
|
|
15
|
+
"loggers": [
|
|
16
|
+
"meteocatpy"
|
|
17
|
+
],
|
|
18
|
+
"requirements": [
|
|
19
|
+
"meteocatpy==1.0.1",
|
|
20
|
+
"packaging>=20.3",
|
|
21
|
+
"wrapt>=1.14.0"
|
|
22
|
+
],
|
|
23
|
+
"version": ""
|
|
13
24
|
}
|
|
@@ -43,6 +43,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
43
43
|
return await self.async_step_update_api_and_limits()
|
|
44
44
|
elif user_input["option"] == "update_limits_only":
|
|
45
45
|
return await self.async_step_update_limits_only()
|
|
46
|
+
elif user_input["option"] == "regenerate_assets":
|
|
47
|
+
return await self.async_step_confirm_regenerate_assets()
|
|
46
48
|
|
|
47
49
|
return self.async_show_form(
|
|
48
50
|
step_id="init",
|
|
@@ -51,7 +53,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
51
53
|
SelectSelectorConfig(
|
|
52
54
|
options=[
|
|
53
55
|
"update_api_and_limits",
|
|
54
|
-
"update_limits_only"
|
|
56
|
+
"update_limits_only",
|
|
57
|
+
"regenerate_assets"
|
|
55
58
|
],
|
|
56
59
|
translation_key="option"
|
|
57
60
|
)
|
|
@@ -173,4 +176,45 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
173
176
|
})
|
|
174
177
|
return self.async_show_form(
|
|
175
178
|
step_id="update_limits_only", data_schema=schema, errors=errors
|
|
176
|
-
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
|
|
182
|
+
"""Confirma si el usuario realmente quiere regenerar los assets."""
|
|
183
|
+
if user_input is not None:
|
|
184
|
+
if user_input.get("confirm") is True:
|
|
185
|
+
return await self.async_step_regenerate_assets()
|
|
186
|
+
else:
|
|
187
|
+
# Volver al menú inicial si el usuario cancela
|
|
188
|
+
return await self.async_step_init()
|
|
189
|
+
|
|
190
|
+
schema = vol.Schema({
|
|
191
|
+
vol.Required("confirm", default=False): bool
|
|
192
|
+
})
|
|
193
|
+
return self.async_show_form(
|
|
194
|
+
step_id="confirm_regenerate_assets",
|
|
195
|
+
data_schema=schema,
|
|
196
|
+
description_placeholders={
|
|
197
|
+
"warning": "Esto regenerará los archivos faltantes de towns.json, stations.json, variables.json, symbols.json y stations_<town_id>.json. ¿Desea continuar?"
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def async_step_regenerate_assets(self, user_input: dict | None = None):
|
|
202
|
+
"""Regenera los archivos de assets."""
|
|
203
|
+
from . import ensure_assets_exist # importamos la función desde __init__.py
|
|
204
|
+
|
|
205
|
+
errors = {}
|
|
206
|
+
try:
|
|
207
|
+
# Llamar a la función que garantiza que los assets existan
|
|
208
|
+
await ensure_assets_exist(self.hass, self._config_entry.data)
|
|
209
|
+
|
|
210
|
+
_LOGGER.info("Archivos de assets regenerados correctamente.")
|
|
211
|
+
# Forzar recarga de la integración
|
|
212
|
+
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
|
|
213
|
+
|
|
214
|
+
return self.async_create_entry(title="", data={})
|
|
215
|
+
|
|
216
|
+
except Exception as ex:
|
|
217
|
+
_LOGGER.error("Error al regenerar assets: %s", ex)
|
|
218
|
+
errors["base"] = "regenerate_failed"
|
|
219
|
+
|
|
220
|
+
return self.async_show_form(step_id="regenerate_assets", errors=errors)
|
|
@@ -1357,6 +1357,13 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1357
1357
|
# Assign entity_category if defined in the description
|
|
1358
1358
|
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1359
1359
|
|
|
1360
|
+
def _map_meteor_case_insensitive(self, meteor: str) -> str:
|
|
1361
|
+
"""Busca el meteor en el mapping sin importar mayúsculas/minúsculas."""
|
|
1362
|
+
for key, value in self.METEOR_MAPPING.items():
|
|
1363
|
+
if key.lower() == meteor.lower():
|
|
1364
|
+
return value
|
|
1365
|
+
return "unknown"
|
|
1366
|
+
|
|
1360
1367
|
@property
|
|
1361
1368
|
def native_value(self):
|
|
1362
1369
|
"""Devuelve el número de alertas activas."""
|
|
@@ -1368,10 +1375,13 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1368
1375
|
meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
|
|
1369
1376
|
|
|
1370
1377
|
# Convertimos las claves al formato deseado usando el mapping
|
|
1371
|
-
attributes = {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1378
|
+
attributes = {}
|
|
1379
|
+
for i, meteor in enumerate(meteor_details.keys()):
|
|
1380
|
+
mapped_name = self._map_meteor_case_insensitive(meteor)
|
|
1381
|
+
if not mapped_name:
|
|
1382
|
+
_LOGGER.warning("Meteor desconocido sin mapeo: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertRegionSensor' si es necesario.", meteor)
|
|
1383
|
+
mapped_name = "unknown"
|
|
1384
|
+
attributes[f"alert_{i+1}"] = mapped_name
|
|
1375
1385
|
|
|
1376
1386
|
_LOGGER.info("Atributos traducidos del sensor: %s", attributes)
|
|
1377
1387
|
return attributes
|
|
@@ -1461,6 +1471,23 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1461
1471
|
self._attr_unique_id,
|
|
1462
1472
|
)
|
|
1463
1473
|
|
|
1474
|
+
def _get_meteor_data_case_insensitive(self, meteor_type: str) -> dict:
|
|
1475
|
+
"""Busca en los datos de meteor de forma case-insensitive."""
|
|
1476
|
+
meteor_data_dict = self.coordinator.data.get("detalles", {}).get("meteor", {})
|
|
1477
|
+
for key, value in meteor_data_dict.items():
|
|
1478
|
+
if key.lower() == meteor_type.lower():
|
|
1479
|
+
return value
|
|
1480
|
+
return {}
|
|
1481
|
+
|
|
1482
|
+
def _get_umbral_case_insensitive(self, umbral: str) -> str:
|
|
1483
|
+
"""Convierte un umbral a su clave interna usando case-insensitive."""
|
|
1484
|
+
if umbral is None:
|
|
1485
|
+
return "unknown"
|
|
1486
|
+
for key, value in self.UMBRAL_MAPPING.items():
|
|
1487
|
+
if key.lower() == umbral.lower():
|
|
1488
|
+
return value
|
|
1489
|
+
return "unknown"
|
|
1490
|
+
|
|
1464
1491
|
@property
|
|
1465
1492
|
def native_value(self):
|
|
1466
1493
|
"""Devuelve el estado de la alerta específica."""
|
|
@@ -1468,7 +1495,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1468
1495
|
if not meteor_type:
|
|
1469
1496
|
return "Desconocido"
|
|
1470
1497
|
|
|
1471
|
-
meteor_data = self.
|
|
1498
|
+
meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
|
|
1472
1499
|
|
|
1473
1500
|
# Convertir estado para translation_key
|
|
1474
1501
|
estado_original = meteor_data.get("estado", "Tancat")
|
|
@@ -1479,15 +1506,27 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
|
|
|
1479
1506
|
"""Devuelve los atributos específicos de la alerta."""
|
|
1480
1507
|
meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
|
|
1481
1508
|
if not meteor_type:
|
|
1482
|
-
|
|
1509
|
+
_LOGGER.warning(
|
|
1510
|
+
"Tipo de meteor desconocido para sensor %s: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
|
|
1511
|
+
self.entity_description.key,
|
|
1512
|
+
self.coordinator.data.get("detalles", {}).get("meteor", {}).keys(),
|
|
1513
|
+
)
|
|
1514
|
+
return "unknown"
|
|
1483
1515
|
|
|
1484
|
-
meteor_data = self.
|
|
1516
|
+
meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
|
|
1485
1517
|
if not meteor_data:
|
|
1486
1518
|
return {}
|
|
1487
1519
|
|
|
1488
1520
|
# Convertir umbral para translation_key
|
|
1489
1521
|
umbral_original = meteor_data.get("umbral")
|
|
1490
|
-
umbral_convertido = self.
|
|
1522
|
+
umbral_convertido = self._get_umbral_case_insensitive(umbral_original)
|
|
1523
|
+
|
|
1524
|
+
if umbral_convertido == "unknown" and umbral_original is not None:
|
|
1525
|
+
_LOGGER.warning(
|
|
1526
|
+
"Umbral desconocido para sensor %s: '%s'. Añadirlo a 'UMBRAL_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
|
|
1527
|
+
self.entity_description.key,
|
|
1528
|
+
umbral_original
|
|
1529
|
+
)
|
|
1491
1530
|
|
|
1492
1531
|
return {
|
|
1493
1532
|
"inicio": meteor_data.get("inicio"),
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"options": {
|
|
37
37
|
"step":{
|
|
38
38
|
"init": {
|
|
39
|
-
"description": "Setup the API Key
|
|
39
|
+
"description": "Setup the API Key, the API limits and regenerate 'assets' files.",
|
|
40
40
|
"title": "Setup options",
|
|
41
41
|
"data": {
|
|
42
42
|
"option": "Options"
|
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
"update_limits_only": {
|
|
50
50
|
"description": "Setup the API limits.",
|
|
51
51
|
"title": "API limits"
|
|
52
|
+
},
|
|
53
|
+
"confirm_regenerate_assets": {
|
|
54
|
+
"description": "Regenerate missing assets files.",
|
|
55
|
+
"title": "Regenerate assets",
|
|
56
|
+
"data": {
|
|
57
|
+
"confirm": "Confirm"
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
},
|
|
54
61
|
"error": {
|
|
@@ -61,7 +68,8 @@
|
|
|
61
68
|
"option": {
|
|
62
69
|
"options": {
|
|
63
70
|
"update_api_and_limits": "Update API Key and limits.",
|
|
64
|
-
"update_limits_only": "Update API limits."
|
|
71
|
+
"update_limits_only": "Update API limits.",
|
|
72
|
+
"regenerate_assets": "Regenerate 'assets' files."
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"options": {
|
|
37
37
|
"step":{
|
|
38
38
|
"init": {
|
|
39
|
-
"description": "Configura l'API Key
|
|
39
|
+
"description": "Configura l'API Key, els límits de l'API i regenera els arxius a 'assets'.",
|
|
40
40
|
"title": "Opcions de configuració",
|
|
41
41
|
"data": {
|
|
42
42
|
"option": "Opcions"
|
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
"update_limits_only": {
|
|
50
50
|
"description": "Configura els límits de l'API.",
|
|
51
51
|
"title": "Límits de l'API"
|
|
52
|
+
},
|
|
53
|
+
"confirm_regenerate_assets": {
|
|
54
|
+
"description": "Regenera els arxius a 'assets'.",
|
|
55
|
+
"title": "Regenera 'assets'",
|
|
56
|
+
"data": {
|
|
57
|
+
"confirm": "Confirmar"
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
},
|
|
54
61
|
"error": {
|
|
@@ -61,7 +68,8 @@
|
|
|
61
68
|
"option": {
|
|
62
69
|
"options": {
|
|
63
70
|
"update_api_and_limits": "Actualitzar API Key i limits.",
|
|
64
|
-
"update_limits_only": "Actualitzar API limits."
|
|
71
|
+
"update_limits_only": "Actualitzar API limits.",
|
|
72
|
+
"regenerate_assets": "Regenera arxius a 'assets'."
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"options": {
|
|
37
37
|
"step":{
|
|
38
38
|
"init": {
|
|
39
|
-
"description": "Setup the API Key
|
|
39
|
+
"description": "Setup the API Key, the API limits and regenerate 'assets' files.",
|
|
40
40
|
"title": "Setup options",
|
|
41
41
|
"data": {
|
|
42
42
|
"option": "Options"
|
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
"update_limits_only": {
|
|
50
50
|
"description": "Setup the API limits.",
|
|
51
51
|
"title": "API limits"
|
|
52
|
+
},
|
|
53
|
+
"confirm_regenerate_assets": {
|
|
54
|
+
"description": "Regenerate missing assets files.",
|
|
55
|
+
"title": "Regenerate assets",
|
|
56
|
+
"data": {
|
|
57
|
+
"confirm": "Confirm"
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
},
|
|
54
61
|
"error": {
|
|
@@ -61,7 +68,8 @@
|
|
|
61
68
|
"option": {
|
|
62
69
|
"options": {
|
|
63
70
|
"update_api_and_limits": "Update API Key and limits.",
|
|
64
|
-
"update_limits_only": "Update API limits."
|
|
71
|
+
"update_limits_only": "Update API limits.",
|
|
72
|
+
"regenerate_assets": "Regenerate 'assets' files."
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"options": {
|
|
37
37
|
"step":{
|
|
38
38
|
"init": {
|
|
39
|
-
"description": "Configura el API Key
|
|
39
|
+
"description": "Configura el API Key, los límites de la API y regenera los archivos en 'assets'.",
|
|
40
40
|
"title": "Opciones de configuración",
|
|
41
41
|
"data": {
|
|
42
42
|
"option": "Opciones"
|
|
@@ -49,6 +49,13 @@
|
|
|
49
49
|
"update_limits_only": {
|
|
50
50
|
"description": "Configura los límites de la API.",
|
|
51
51
|
"title": "Límites de la API"
|
|
52
|
+
},
|
|
53
|
+
"confirm_regenerate_assets": {
|
|
54
|
+
"description": "Regenera los archivos de 'assets'.",
|
|
55
|
+
"title": "Regenera 'assets'",
|
|
56
|
+
"data": {
|
|
57
|
+
"confirm": "Confirmar"
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
},
|
|
54
61
|
"error": {
|
|
@@ -61,7 +68,8 @@
|
|
|
61
68
|
"option": {
|
|
62
69
|
"options": {
|
|
63
70
|
"update_api_and_limits": "Actualizar API Key y límites.",
|
|
64
|
-
"update_limits_only": "Actualizar API límites."
|
|
71
|
+
"update_limits_only": "Actualizar API límites.",
|
|
72
|
+
"regenerate_assets": "Regenerar archivos de 'assets'."
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
},
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
__version__ = "2.2.6"
|
|
1
|
+
__version__ = ""
|
package/filetree.txt
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
└── .github/
|
|
2
|
+
└── ISSUE_TEMPLATE/
|
|
3
|
+
├── bug_report.md
|
|
4
|
+
├── config.yml
|
|
2
5
|
└── workflows/
|
|
6
|
+
├── autocloser.yaml
|
|
7
|
+
├── close-duplicates.yml
|
|
3
8
|
├── hassfest.yaml
|
|
9
|
+
├── publish-zip.yml
|
|
4
10
|
├── release.yml
|
|
11
|
+
├── stale.yml
|
|
12
|
+
├── sync-gitlab.yml
|
|
5
13
|
├── validate.yaml
|
|
6
14
|
├── .gitignore
|
|
7
15
|
├── .gitlab-ci.yml
|
|
@@ -42,6 +50,7 @@
|
|
|
42
50
|
├── diagnostic_sensors.png
|
|
43
51
|
├── dynamic_sensors.png
|
|
44
52
|
├── login.png
|
|
53
|
+
├── options.png
|
|
45
54
|
├── pick_area.png
|
|
46
55
|
├── pick_station.png
|
|
47
56
|
├── pick_town.png
|
package/hacs.json
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
package/pyproject.toml
CHANGED
package/.releaserc.toml
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
branches = ["master"]
|
|
2
|
-
|
|
3
|
-
[plugins]
|
|
4
|
-
"@semantic-release/gitlab" = {}
|
|
5
|
-
"@semantic-release/github" = {}
|
|
6
|
-
"@semantic-release/changelog" = {}
|
|
7
|
-
"@semantic-release/commit-analyzer" = { preset = "conventional" }
|
|
8
|
-
"@semantic-release/release-notes-generator" = {}
|
|
9
|
-
"@semantic-release/git" = {}
|
|
10
|
-
|
|
11
|
-
[[plugins]]
|
|
12
|
-
path = "@semantic-release/git"
|
|
13
|
-
assets = ["meteocat/version.py"]
|
|
14
|
-
message = "chore(release): update version to ${nextRelease.version}"
|
package/releaserc.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"branches": ["master"],
|
|
3
|
-
"plugins": [
|
|
4
|
-
"@semantic-release/commit-analyzer",
|
|
5
|
-
"@semantic-release/release-notes-generator",
|
|
6
|
-
"@semantic-release/changelog",
|
|
7
|
-
[
|
|
8
|
-
"@semantic-release/exec",
|
|
9
|
-
{
|
|
10
|
-
"prepareCmd": "python setup.py sdist bdist_wheel",
|
|
11
|
-
"publishCmd": "twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD}"
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"@semantic-release/github",
|
|
15
|
-
"@semantic-release/git"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
|