meteoalarmapi 0.4.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.
- meteoalarmapi-0.4.0/.gitignore +123 -0
- meteoalarmapi-0.4.0/CHANGELOG.md +60 -0
- meteoalarmapi-0.4.0/LICENSE +21 -0
- meteoalarmapi-0.4.0/PKG-INFO +81 -0
- meteoalarmapi-0.4.0/README.md +60 -0
- meteoalarmapi-0.4.0/meteoalarmapi/__init__.py +8 -0
- meteoalarmapi-0.4.0/meteoalarmapi/meteoalert.py +173 -0
- meteoalarmapi-0.4.0/pyproject.toml +33 -0
- meteoalarmapi-0.4.0/tests/__init__.py +0 -0
- meteoalarmapi-0.4.0/tests/france_sample.xml +379 -0
- meteoalarmapi-0.4.0/tests/france_sample_detail.xml +147 -0
- meteoalarmapi-0.4.0/tests/ireland_sample.xml +6763 -0
- meteoalarmapi-0.4.0/tests/ireland_sample_detail.xml +149 -0
- meteoalarmapi-0.4.0/tests/norway_sample.xml +121 -0
- meteoalarmapi-0.4.0/tests/norway_sample_detail_15082db6.xml +126 -0
- meteoalarmapi-0.4.0/tests/norway_sample_detail_7be0d389.xml +172 -0
- meteoalarmapi-0.4.0/tests/norway_sample_detail_ef68e1fd.xml +164 -0
- meteoalarmapi-0.4.0/tests/test_meteoalarmapi.py +242 -0
- meteoalarmapi-0.4.0/tests/uk_multiple_detail_amber_wind.xml +74 -0
- meteoalarmapi-0.4.0/tests/uk_multiple_detail_yellow_rain.xml +61 -0
- meteoalarmapi-0.4.0/tests/uk_multiple_detail_yellow_wind.xml +74 -0
- meteoalarmapi-0.4.0/tests/uk_multiple_sample.xml +95 -0
- meteoalarmapi-0.4.0/tests/uk_nothern_ireland_sample_detail.xml +106 -0
- meteoalarmapi-0.4.0/tests/uk_sample.xml +769 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
|
|
53
|
+
# Translations
|
|
54
|
+
*.mo
|
|
55
|
+
*.pot
|
|
56
|
+
|
|
57
|
+
# Django stuff:
|
|
58
|
+
*.log
|
|
59
|
+
local_settings.py
|
|
60
|
+
db.sqlite3
|
|
61
|
+
|
|
62
|
+
# Flask stuff:
|
|
63
|
+
instance/
|
|
64
|
+
.webassets-cache
|
|
65
|
+
|
|
66
|
+
# Scrapy stuff:
|
|
67
|
+
.scrapy
|
|
68
|
+
|
|
69
|
+
# Sphinx documentation
|
|
70
|
+
docs/_build/
|
|
71
|
+
|
|
72
|
+
# PyBuilder
|
|
73
|
+
target/
|
|
74
|
+
|
|
75
|
+
# Jupyter Notebook
|
|
76
|
+
.ipynb_checkpoints
|
|
77
|
+
|
|
78
|
+
# IPython
|
|
79
|
+
profile_default/
|
|
80
|
+
ipython_config.py
|
|
81
|
+
|
|
82
|
+
# pyenv
|
|
83
|
+
.python-version
|
|
84
|
+
|
|
85
|
+
# pipenv
|
|
86
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
87
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
88
|
+
# having no cross-platform support, pipenv may install dependencies that don’t work, or not
|
|
89
|
+
# install all needed dependencies.
|
|
90
|
+
#Pipfile.lock
|
|
91
|
+
|
|
92
|
+
# celery beat schedule file
|
|
93
|
+
celerybeat-schedule
|
|
94
|
+
|
|
95
|
+
# SageMath parsed files
|
|
96
|
+
*.sage.py
|
|
97
|
+
|
|
98
|
+
# Environments
|
|
99
|
+
.env
|
|
100
|
+
.venv
|
|
101
|
+
env/
|
|
102
|
+
venv/
|
|
103
|
+
ENV/
|
|
104
|
+
env.bak/
|
|
105
|
+
venv.bak/
|
|
106
|
+
|
|
107
|
+
# Spyder project settings
|
|
108
|
+
.spyderproject
|
|
109
|
+
.spyproject
|
|
110
|
+
|
|
111
|
+
# Rope project settings
|
|
112
|
+
.ropeproject
|
|
113
|
+
|
|
114
|
+
# mkdocs documentation
|
|
115
|
+
/site
|
|
116
|
+
|
|
117
|
+
# mypy
|
|
118
|
+
.mypy_cache/
|
|
119
|
+
.dmypy.json
|
|
120
|
+
dmypy.json
|
|
121
|
+
|
|
122
|
+
# Pyre type checker
|
|
123
|
+
.pyre/
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
|
+
|
|
7
|
+
## meteoalarmapi 0.4.0 - 2026-11-11
|
|
8
|
+
First release of the maintained fork, published as `meteoalarmapi`.
|
|
9
|
+
|
|
10
|
+
- Renamed the package to `meteoalarmapi` (`from meteoalarmapi import Meteoalert`)
|
|
11
|
+
- `get_alert()` returns the most severe current alert instead of the first match in the feed
|
|
12
|
+
- Added `get_alerts()`, which returns every matching alert, most severe first
|
|
13
|
+
- `get_alerts(include_expired=False)` leaves out expired alerts, which some feeds keep for a while; future alerts are kept
|
|
14
|
+
- Fixed CAP parameters (awareness_level, awareness_type, ...) being dropped for single-language feeds
|
|
15
|
+
- Alerts include the CAP message fields `identifier`, `message_type`, `references` and `sent`
|
|
16
|
+
- Alerts with several feed entries (per language or area) are fetched and returned once
|
|
17
|
+
- A warning whose CAP document has gone (404/410) is skipped instead of raising `WrongCountry`, and the feed's 404 message mentions that MeteoAlarm may be temporarily unavailable (home-assistant/core#177632)
|
|
18
|
+
- Dropped Python 2 and versions older than 3.10; packaging moved to pyproject.toml
|
|
19
|
+
- Releases are built and published to PyPI by GitHub Actions
|
|
20
|
+
|
|
21
|
+
## 0.3.1 - 2024-02-07
|
|
22
|
+
###
|
|
23
|
+
- Match provinces by their EMMA_ID code as well as by name
|
|
24
|
+
|
|
25
|
+
## 0.0.1 - 2019-04-29
|
|
26
|
+
###
|
|
27
|
+
- Initial release
|
|
28
|
+
|
|
29
|
+
## 0.1.6 - 2019-07-12
|
|
30
|
+
###
|
|
31
|
+
- Fix issue with xml to look also over attributes.
|
|
32
|
+
|
|
33
|
+
## 0.1.7 - 2020-01-21
|
|
34
|
+
###
|
|
35
|
+
- Handle feed with one entry
|
|
36
|
+
|
|
37
|
+
## 0.1.8 - 2021-06-02
|
|
38
|
+
###
|
|
39
|
+
This is a breaking version, please modify your country!!!
|
|
40
|
+
It's now possible to add all european country that is on the website on
|
|
41
|
+
meteoalarm.org.
|
|
42
|
+
|
|
43
|
+
- Change the api from meteoalarm.eu to meteoalarm.orge
|
|
44
|
+
|
|
45
|
+
## 0.1.9 - 2021-06-07
|
|
46
|
+
###
|
|
47
|
+
Several (bug) fixes
|
|
48
|
+
|
|
49
|
+
- Add timeout for the api to meteoalarm
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## 0.2.0 - 2021-06-07
|
|
53
|
+
###
|
|
54
|
+
Bump to version 0.2.0
|
|
55
|
+
|
|
56
|
+
## 0.3.0 - 2022-07-27
|
|
57
|
+
###
|
|
58
|
+
Bug fix
|
|
59
|
+
|
|
60
|
+
- Thank @attila123456 to fix the fetching bug
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Rolf Berkenbosch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meteoalarmapi
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Weather alerts for European regions from MeteoAlarm.org. A maintained fork of meteoalertapi.
|
|
5
|
+
Project-URL: Source, https://github.com/mastermc0/meteoalarmapi
|
|
6
|
+
Project-URL: Issues, https://github.com/mastermc0/meteoalarmapi/issues
|
|
7
|
+
Project-URL: Original, https://github.com/rolfberkenbosch/meteoalert-api
|
|
8
|
+
Author: Rolf Berkenbosch
|
|
9
|
+
Maintainer: mastermc0
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Requires-Dist: xmltodict
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == 'test'
|
|
19
|
+
Requires-Dist: requests-mock; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# meteoalarmapi
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
An unofficial Python wrapper for [MeteoAlarm.org](https://meteoalarm.org) (European weather alerts), which can be used to check whether a given province in your country currently has a weather alert.
|
|
27
|
+
|
|
28
|
+
This is a maintained fork of [meteoalertapi](https://github.com/rolfberkenbosch/meteoalert-api) by Rolf Berkenbosch, which has had no releases since February 2024. It keeps the same `Meteoalert` class and alert fields. See [What changed from meteoalertapi](#what-changed-from-meteoalertapi).
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```console
|
|
33
|
+
pip install meteoalarmapi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Code example
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from meteoalarmapi import Meteoalert
|
|
40
|
+
|
|
41
|
+
# Find your country and province on https://meteoalarm.org/ or https://feeds.meteoalarm.org/
|
|
42
|
+
# The province can be its name (matched as a regular expression) or its EMMA_ID code.
|
|
43
|
+
meteo = Meteoalert('united-kingdom', 'Highlands & Eilean Siar')
|
|
44
|
+
|
|
45
|
+
# The most severe current alert, or {} when there is none
|
|
46
|
+
print(meteo.get_alert())
|
|
47
|
+
|
|
48
|
+
# Every alert for the province, most severe first
|
|
49
|
+
for alert in meteo.get_alerts():
|
|
50
|
+
print(alert['event'], alert['severity'], alert['onset'], alert['expires'])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each alert also has the CAP message fields `identifier`, `message_type` (`Alert`, `Update` or `Cancel`), `references` and `sent`, so you can follow a warning across updates and tell when one is cancelled.
|
|
54
|
+
|
|
55
|
+
Some feeds keep warnings for a while after they end. Pass `include_expired=False` to leave expired alerts out. Future alerts, which haven't started yet, are kept:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
for alert in meteo.get_alerts(include_expired=False):
|
|
59
|
+
print(alert['event'], alert['onset'], alert['expires'])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Alerts are ranked with current ones first, then by awareness level (`awareness_level`, e.g. `3; Orange; Severe`), then by CAP severity. Alerts that rank equally keep their order in the feed.
|
|
63
|
+
|
|
64
|
+
## What changed from meteoalertapi
|
|
65
|
+
|
|
66
|
+
- The package is now imported as `meteoalarmapi` instead of `meteoalertapi`.
|
|
67
|
+
- `get_alert()` returns the most severe current alert. Before, it returned the first match in the feed, so a yellow warning could hide an amber one.
|
|
68
|
+
- New `get_alerts()` returns every matching alert, most severe first.
|
|
69
|
+
- `get_alerts(include_expired=False)` leaves out expired alerts, which some feeds keep for a while. Future alerts are kept.
|
|
70
|
+
- `awareness_level`, `awareness_type` and the other CAP parameters are now included for feeds with a single language, such as the UK's. Before, they were silently dropped.
|
|
71
|
+
- An alert listed several times in the feed (once per language or area) is returned and fetched only once.
|
|
72
|
+
- Each alert includes the CAP `identifier`, `message_type`, `references` and `sent`.
|
|
73
|
+
- Python 3.10 or newer is required.
|
|
74
|
+
|
|
75
|
+
## Changelog
|
|
76
|
+
|
|
77
|
+
See the [CHANGELOG](./CHANGELOG.md) file.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT. Original work copyright (c) 2018 Rolf Berkenbosch.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# meteoalarmapi
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
An unofficial Python wrapper for [MeteoAlarm.org](https://meteoalarm.org) (European weather alerts), which can be used to check whether a given province in your country currently has a weather alert.
|
|
6
|
+
|
|
7
|
+
This is a maintained fork of [meteoalertapi](https://github.com/rolfberkenbosch/meteoalert-api) by Rolf Berkenbosch, which has had no releases since February 2024. It keeps the same `Meteoalert` class and alert fields. See [What changed from meteoalertapi](#what-changed-from-meteoalertapi).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
pip install meteoalarmapi
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Code example
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from meteoalarmapi import Meteoalert
|
|
19
|
+
|
|
20
|
+
# Find your country and province on https://meteoalarm.org/ or https://feeds.meteoalarm.org/
|
|
21
|
+
# The province can be its name (matched as a regular expression) or its EMMA_ID code.
|
|
22
|
+
meteo = Meteoalert('united-kingdom', 'Highlands & Eilean Siar')
|
|
23
|
+
|
|
24
|
+
# The most severe current alert, or {} when there is none
|
|
25
|
+
print(meteo.get_alert())
|
|
26
|
+
|
|
27
|
+
# Every alert for the province, most severe first
|
|
28
|
+
for alert in meteo.get_alerts():
|
|
29
|
+
print(alert['event'], alert['severity'], alert['onset'], alert['expires'])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Each alert also has the CAP message fields `identifier`, `message_type` (`Alert`, `Update` or `Cancel`), `references` and `sent`, so you can follow a warning across updates and tell when one is cancelled.
|
|
33
|
+
|
|
34
|
+
Some feeds keep warnings for a while after they end. Pass `include_expired=False` to leave expired alerts out. Future alerts, which haven't started yet, are kept:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
for alert in meteo.get_alerts(include_expired=False):
|
|
38
|
+
print(alert['event'], alert['onset'], alert['expires'])
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Alerts are ranked with current ones first, then by awareness level (`awareness_level`, e.g. `3; Orange; Severe`), then by CAP severity. Alerts that rank equally keep their order in the feed.
|
|
42
|
+
|
|
43
|
+
## What changed from meteoalertapi
|
|
44
|
+
|
|
45
|
+
- The package is now imported as `meteoalarmapi` instead of `meteoalertapi`.
|
|
46
|
+
- `get_alert()` returns the most severe current alert. Before, it returned the first match in the feed, so a yellow warning could hide an amber one.
|
|
47
|
+
- New `get_alerts()` returns every matching alert, most severe first.
|
|
48
|
+
- `get_alerts(include_expired=False)` leaves out expired alerts, which some feeds keep for a while. Future alerts are kept.
|
|
49
|
+
- `awareness_level`, `awareness_type` and the other CAP parameters are now included for feeds with a single language, such as the UK's. Before, they were silently dropped.
|
|
50
|
+
- An alert listed several times in the feed (once per language or area) is returned and fetched only once.
|
|
51
|
+
- Each alert includes the CAP `identifier`, `message_type`, `references` and `sent`.
|
|
52
|
+
- Python 3.10 or newer is required.
|
|
53
|
+
|
|
54
|
+
## Changelog
|
|
55
|
+
|
|
56
|
+
See the [CHANGELOG](./CHANGELOG.md) file.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT. Original work copyright (c) 2018 Rolf Berkenbosch.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Weather alerts for a European region from MeteoAlarm.org.
|
|
2
|
+
|
|
3
|
+
Reads the country's legacy Atom feed, keeps the entries whose area matches
|
|
4
|
+
the region (by name or EMMA_ID code) and fetches each one's CAP document.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
import xmltodict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WrongCountry(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ServerError(Exception):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnexpectedError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# CAP severities, least to most severe
|
|
27
|
+
SEVERITIES = ["Unknown", "Minor", "Moderate", "Severe", "Extreme"]
|
|
28
|
+
TIMEOUT = 10
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _utcnow():
|
|
32
|
+
# A function so tests can patch the clock
|
|
33
|
+
return datetime.now(timezone.utc)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Meteoalert:
|
|
37
|
+
|
|
38
|
+
endpoint = "https://feeds.meteoalarm.org/feeds/meteoalarm-legacy-atom-{0}"
|
|
39
|
+
|
|
40
|
+
def __init__(self, country, province, language="en-GB"):
|
|
41
|
+
self.country = country.lower()
|
|
42
|
+
self.province = province
|
|
43
|
+
self.language = language
|
|
44
|
+
|
|
45
|
+
def get_alert(self):
|
|
46
|
+
"""The most severe alert for the province, or {} when there is none.
|
|
47
|
+
|
|
48
|
+
See get_alerts() for the order.
|
|
49
|
+
"""
|
|
50
|
+
alerts = self.get_alerts()
|
|
51
|
+
return alerts[0] if alerts else {}
|
|
52
|
+
|
|
53
|
+
def get_alerts(self, include_expired=True):
|
|
54
|
+
"""Every alert for the province, most severe first.
|
|
55
|
+
|
|
56
|
+
Unexpired alerts come first, then the highest awareness level (or CAP
|
|
57
|
+
severity when there is none). Equally ranked alerts keep feed order.
|
|
58
|
+
|
|
59
|
+
With include_expired=False, expired alerts are left out. Future
|
|
60
|
+
alerts (not started yet) are kept, so upcoming warnings still show.
|
|
61
|
+
"""
|
|
62
|
+
feed = xmltodict.parse(self._fetch(self.endpoint.format(self.country)))
|
|
63
|
+
entries = (feed.get("feed") or {}).get("entry", [])
|
|
64
|
+
if not isinstance(entries, list):
|
|
65
|
+
entries = [entries]
|
|
66
|
+
|
|
67
|
+
alerts = []
|
|
68
|
+
# The feed has an entry per language and area of an alert, all
|
|
69
|
+
# pointing at the same CAP document, so fetch each one once.
|
|
70
|
+
seen = set()
|
|
71
|
+
for entry in entries:
|
|
72
|
+
if not self.is_location_match(entry, self.province):
|
|
73
|
+
continue
|
|
74
|
+
links = entry.get("link", [])
|
|
75
|
+
if not isinstance(links, list):
|
|
76
|
+
links = [links]
|
|
77
|
+
cap_url = next((link.get("@href") for link in links
|
|
78
|
+
if link.get("@type") == "application/cap+xml"), None)
|
|
79
|
+
if cap_url and cap_url not in seen:
|
|
80
|
+
seen.add(cap_url)
|
|
81
|
+
text = self._fetch(cap_url, missing_ok=True)
|
|
82
|
+
# A warning withdrawn between reading the feed and fetching its
|
|
83
|
+
# document is gone, not a sign of a wrong country, so skip it.
|
|
84
|
+
if text is not None:
|
|
85
|
+
alerts.append(self._parse_cap(text))
|
|
86
|
+
|
|
87
|
+
if not include_expired:
|
|
88
|
+
# Some feeds keep warnings for a while after they end
|
|
89
|
+
alerts = [a for a in alerts if not self.is_expired(a.get("expires"))]
|
|
90
|
+
|
|
91
|
+
# sorted() is stable, so equally ranked alerts keep feed order
|
|
92
|
+
return sorted(alerts, key=self.alert_rank, reverse=True)
|
|
93
|
+
|
|
94
|
+
def _parse_cap(self, text):
|
|
95
|
+
"""The CAP <info> in the requested language (or the first one) as a dict,
|
|
96
|
+
plus the message fields needed to track an alert across updates"""
|
|
97
|
+
alert = xmltodict.parse(text).get("alert", {})
|
|
98
|
+
infos = alert.get("info", [])
|
|
99
|
+
if not isinstance(infos, list):
|
|
100
|
+
infos = [infos]
|
|
101
|
+
if not infos:
|
|
102
|
+
return {}
|
|
103
|
+
info = next((i for i in infos if self.language in (i.get("language") or "")),
|
|
104
|
+
infos[0])
|
|
105
|
+
|
|
106
|
+
data = {key: value for key, value in info.items() if isinstance(value, str)}
|
|
107
|
+
parameters = info.get("parameter", [])
|
|
108
|
+
if not isinstance(parameters, list):
|
|
109
|
+
parameters = [parameters]
|
|
110
|
+
for parameter in parameters:
|
|
111
|
+
data[parameter.get("valueName")] = parameter.get("value")
|
|
112
|
+
|
|
113
|
+
# Each CAP message has its own identifier; an Update or Cancel lists
|
|
114
|
+
# the messages it replaces in references
|
|
115
|
+
data["identifier"] = alert.get("identifier")
|
|
116
|
+
data["message_type"] = alert.get("msgType")
|
|
117
|
+
data["references"] = alert.get("references")
|
|
118
|
+
data["sent"] = alert.get("sent")
|
|
119
|
+
return data
|
|
120
|
+
|
|
121
|
+
def alert_rank(self, data):
|
|
122
|
+
"""Sort key for an alert: (not expired, awareness level, severity)"""
|
|
123
|
+
level = re.match(r"\s*(\d+)", data.get("awareness_level") or "")
|
|
124
|
+
severity = data.get("severity")
|
|
125
|
+
return (
|
|
126
|
+
not self.is_expired(data.get("expires")),
|
|
127
|
+
int(level.group(1)) if level else 0,
|
|
128
|
+
SEVERITIES.index(severity) if severity in SEVERITIES else 0,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def is_expired(expires):
|
|
133
|
+
"""Whether a CAP time (2024-01-01T12:00:00+01:00) is in the past"""
|
|
134
|
+
try:
|
|
135
|
+
moment = datetime.fromisoformat(expires.replace("Z", "+00:00"))
|
|
136
|
+
except (AttributeError, ValueError):
|
|
137
|
+
return False
|
|
138
|
+
if moment.tzinfo is None:
|
|
139
|
+
moment = moment.replace(tzinfo=timezone.utc)
|
|
140
|
+
return moment <= _utcnow()
|
|
141
|
+
|
|
142
|
+
def is_location_match(self, entry, province):
|
|
143
|
+
# Attempt to match the province first by regex on name
|
|
144
|
+
if re.search(rf"{province}", entry.get("cap:areaDesc") or "", re.IGNORECASE):
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
# then by areaCode
|
|
148
|
+
geocode = entry.get("cap:geocode")
|
|
149
|
+
return isinstance(geocode, dict) and geocode.get("value") == province
|
|
150
|
+
|
|
151
|
+
def _fetch(self, url, missing_ok=False):
|
|
152
|
+
"""The response text, or None for a 404/410 when missing_ok"""
|
|
153
|
+
try:
|
|
154
|
+
response = requests.get(url, timeout=TIMEOUT)
|
|
155
|
+
except Exception as err:
|
|
156
|
+
raise UnexpectedError(str(err)) from err
|
|
157
|
+
if missing_ok and response.status_code in (404, 410):
|
|
158
|
+
return None
|
|
159
|
+
self.check_status_code(response.status_code)
|
|
160
|
+
return response.content.decode("utf-8")
|
|
161
|
+
|
|
162
|
+
def check_status_code(self, status_code):
|
|
163
|
+
if status_code == 404:
|
|
164
|
+
# MeteoAlarm sometimes briefly returns 404 for valid countries too
|
|
165
|
+
raise WrongCountry(
|
|
166
|
+
f"MeteoAlarm has no feed for country {self.country!r} (404). Check the "
|
|
167
|
+
"name matches feeds.meteoalarm.org; if it does, MeteoAlarm may be "
|
|
168
|
+
"temporarily unavailable"
|
|
169
|
+
)
|
|
170
|
+
elif status_code >= 500:
|
|
171
|
+
raise ServerError(f"Server error - status code: {status_code}")
|
|
172
|
+
elif status_code != 200:
|
|
173
|
+
raise UnexpectedError(f"Server returned unexpected status code: {status_code}")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "meteoalarmapi"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Weather alerts for European regions from MeteoAlarm.org. A maintained fork of meteoalertapi."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Rolf Berkenbosch" }]
|
|
14
|
+
maintainers = [{ name = "mastermc0" }]
|
|
15
|
+
dependencies = ["requests", "xmltodict"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
test = ["pytest", "requests-mock"]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Source = "https://github.com/mastermc0/meteoalarmapi"
|
|
26
|
+
Issues = "https://github.com/mastermc0/meteoalarmapi/issues"
|
|
27
|
+
Original = "https://github.com/rolfberkenbosch/meteoalert-api"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
include = ["meteoalarmapi", "tests", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 100
|
|
File without changes
|