meteoflow 1.0.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.
- meteoflow-1.0.0/LICENSE +21 -0
- meteoflow-1.0.0/PKG-INFO +120 -0
- meteoflow-1.0.0/README.md +100 -0
- meteoflow-1.0.0/meteoflow/__init__.py +39 -0
- meteoflow-1.0.0/meteoflow/_base.py +95 -0
- meteoflow-1.0.0/meteoflow/air.py +17 -0
- meteoflow-1.0.0/meteoflow/client.py +74 -0
- meteoflow-1.0.0/meteoflow/exceptions.py +36 -0
- meteoflow-1.0.0/meteoflow/geography.py +37 -0
- meteoflow-1.0.0/meteoflow/geomagnetic.py +8 -0
- meteoflow-1.0.0/meteoflow/location.py +46 -0
- meteoflow-1.0.0/meteoflow/options.py +37 -0
- meteoflow-1.0.0/meteoflow/transport.py +124 -0
- meteoflow-1.0.0/meteoflow.egg-info/PKG-INFO +120 -0
- meteoflow-1.0.0/meteoflow.egg-info/SOURCES.txt +22 -0
- meteoflow-1.0.0/meteoflow.egg-info/dependency_links.txt +1 -0
- meteoflow-1.0.0/meteoflow.egg-info/requires.txt +1 -0
- meteoflow-1.0.0/meteoflow.egg-info/top_level.txt +1 -0
- meteoflow-1.0.0/pyproject.toml +29 -0
- meteoflow-1.0.0/setup.cfg +4 -0
- meteoflow-1.0.0/setup.py +25 -0
- meteoflow-1.0.0/tests/test_location.py +34 -0
- meteoflow-1.0.0/tests/test_options.py +38 -0
- meteoflow-1.0.0/tests/test_query.py +45 -0
meteoflow-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeteoFlow
|
|
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.
|
meteoflow-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meteoflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for Meteoflow Weather API
|
|
5
|
+
Author: Meteoflow
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.4
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Requires-Python: >=3.4
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
|
|
21
|
+
# Meteoflow Python SDK
|
|
22
|
+
|
|
23
|
+
Python SDK for Meteoflow Weather API.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install meteoflow
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from meteoflow import WeatherClient, LocationSlug, LocationCoords, LocationIP
|
|
35
|
+
|
|
36
|
+
client = WeatherClient("TOKEN")
|
|
37
|
+
|
|
38
|
+
# Current weather by slug
|
|
39
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
40
|
+
|
|
41
|
+
# Current weather by coords
|
|
42
|
+
current = client.current(LocationCoords(51.5072, -0.1275))
|
|
43
|
+
|
|
44
|
+
# Current weather by IP
|
|
45
|
+
current = client.current(LocationIP("8.8.8.8"))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Forecasts
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from meteoflow import WeatherClient, LocationIP, ForecastOptions
|
|
52
|
+
|
|
53
|
+
client = WeatherClient("TOKEN")
|
|
54
|
+
|
|
55
|
+
# Hourly forecast by IP
|
|
56
|
+
options = ForecastOptions(days=1, units="metric", lang="en")
|
|
57
|
+
forecast = client.forecast_hourly(LocationIP("8.8.8.8"), options)
|
|
58
|
+
|
|
59
|
+
# 3-hour forecast by IP
|
|
60
|
+
options = ForecastOptions(days=2, units="metric", lang="en")
|
|
61
|
+
forecast = client.forecast_3hourly(LocationIP("8.8.8.8"), options)
|
|
62
|
+
|
|
63
|
+
# Daily forecast by IP
|
|
64
|
+
options = ForecastOptions(days=5, units="metric", lang="en")
|
|
65
|
+
forecast = client.forecast_daily(LocationIP("8.8.8.8"), options)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Air Quality
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from meteoflow import AirQualityClient, LocationCoords, ForecastOptions
|
|
72
|
+
|
|
73
|
+
client = AirQualityClient("TOKEN")
|
|
74
|
+
|
|
75
|
+
air = client.by_days(LocationCoords(51.5072, -0.1275), ForecastOptions(days=3))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Geomagnetic
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from meteoflow import GeomagneticClient, LocationSlug
|
|
82
|
+
|
|
83
|
+
client = GeomagneticClient("TOKEN")
|
|
84
|
+
|
|
85
|
+
geo = client.by_days(LocationSlug("united-kingdom-london"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Geography Search → Weather
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from meteoflow import GeographyClient, WeatherClient, LocationSlug
|
|
92
|
+
|
|
93
|
+
geo = GeographyClient("TOKEN")
|
|
94
|
+
weather = WeatherClient("TOKEN")
|
|
95
|
+
|
|
96
|
+
results = geo.search("London", limit=1)
|
|
97
|
+
slug = results["items"][0]["slug"]
|
|
98
|
+
|
|
99
|
+
current = weather.current(LocationSlug(slug))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Auth: Header vs Query
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from meteoflow import WeatherClient, LocationSlug
|
|
106
|
+
|
|
107
|
+
# Default: X-token header
|
|
108
|
+
client = WeatherClient("TOKEN")
|
|
109
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
110
|
+
|
|
111
|
+
# Query auth: ?key=TOKEN
|
|
112
|
+
client_q = WeatherClient("TOKEN", auth_in_query=True)
|
|
113
|
+
current_q = client_q.current(LocationSlug("united-kingdom-london"))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Notes
|
|
117
|
+
|
|
118
|
+
- Supported Python versions: 3.4+
|
|
119
|
+
- Sync only in core package
|
|
120
|
+
- Returns parsed JSON as `dict`
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Meteoflow Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for Meteoflow Weather API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install meteoflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from meteoflow import WeatherClient, LocationSlug, LocationCoords, LocationIP
|
|
15
|
+
|
|
16
|
+
client = WeatherClient("TOKEN")
|
|
17
|
+
|
|
18
|
+
# Current weather by slug
|
|
19
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
20
|
+
|
|
21
|
+
# Current weather by coords
|
|
22
|
+
current = client.current(LocationCoords(51.5072, -0.1275))
|
|
23
|
+
|
|
24
|
+
# Current weather by IP
|
|
25
|
+
current = client.current(LocationIP("8.8.8.8"))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Forecasts
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from meteoflow import WeatherClient, LocationIP, ForecastOptions
|
|
32
|
+
|
|
33
|
+
client = WeatherClient("TOKEN")
|
|
34
|
+
|
|
35
|
+
# Hourly forecast by IP
|
|
36
|
+
options = ForecastOptions(days=1, units="metric", lang="en")
|
|
37
|
+
forecast = client.forecast_hourly(LocationIP("8.8.8.8"), options)
|
|
38
|
+
|
|
39
|
+
# 3-hour forecast by IP
|
|
40
|
+
options = ForecastOptions(days=2, units="metric", lang="en")
|
|
41
|
+
forecast = client.forecast_3hourly(LocationIP("8.8.8.8"), options)
|
|
42
|
+
|
|
43
|
+
# Daily forecast by IP
|
|
44
|
+
options = ForecastOptions(days=5, units="metric", lang="en")
|
|
45
|
+
forecast = client.forecast_daily(LocationIP("8.8.8.8"), options)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Air Quality
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from meteoflow import AirQualityClient, LocationCoords, ForecastOptions
|
|
52
|
+
|
|
53
|
+
client = AirQualityClient("TOKEN")
|
|
54
|
+
|
|
55
|
+
air = client.by_days(LocationCoords(51.5072, -0.1275), ForecastOptions(days=3))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Geomagnetic
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from meteoflow import GeomagneticClient, LocationSlug
|
|
62
|
+
|
|
63
|
+
client = GeomagneticClient("TOKEN")
|
|
64
|
+
|
|
65
|
+
geo = client.by_days(LocationSlug("united-kingdom-london"))
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Geography Search → Weather
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from meteoflow import GeographyClient, WeatherClient, LocationSlug
|
|
72
|
+
|
|
73
|
+
geo = GeographyClient("TOKEN")
|
|
74
|
+
weather = WeatherClient("TOKEN")
|
|
75
|
+
|
|
76
|
+
results = geo.search("London", limit=1)
|
|
77
|
+
slug = results["items"][0]["slug"]
|
|
78
|
+
|
|
79
|
+
current = weather.current(LocationSlug(slug))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Auth: Header vs Query
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from meteoflow import WeatherClient, LocationSlug
|
|
86
|
+
|
|
87
|
+
# Default: X-token header
|
|
88
|
+
client = WeatherClient("TOKEN")
|
|
89
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
90
|
+
|
|
91
|
+
# Query auth: ?key=TOKEN
|
|
92
|
+
client_q = WeatherClient("TOKEN", auth_in_query=True)
|
|
93
|
+
current_q = client_q.current(LocationSlug("united-kingdom-london"))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Notes
|
|
97
|
+
|
|
98
|
+
- Supported Python versions: 3.4+
|
|
99
|
+
- Sync only in core package
|
|
100
|
+
- Returns parsed JSON as `dict`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from .air import AirQualityClient
|
|
2
|
+
from .client import WeatherClient
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
ApiError,
|
|
5
|
+
AuthError,
|
|
6
|
+
NetworkError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
SdkError,
|
|
10
|
+
ServerError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from .geomagnetic import GeomagneticClient
|
|
14
|
+
from .geography import GeographyClient
|
|
15
|
+
from .location import LocationCoords, LocationIP, LocationSlug
|
|
16
|
+
from .options import ForecastOptions, UNITS_IMPERIAL, UNITS_METRIC
|
|
17
|
+
from .transport import HttpTransport
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"WeatherClient",
|
|
21
|
+
"AirQualityClient",
|
|
22
|
+
"GeomagneticClient",
|
|
23
|
+
"GeographyClient",
|
|
24
|
+
"LocationSlug",
|
|
25
|
+
"LocationCoords",
|
|
26
|
+
"LocationIP",
|
|
27
|
+
"ForecastOptions",
|
|
28
|
+
"UNITS_METRIC",
|
|
29
|
+
"UNITS_IMPERIAL",
|
|
30
|
+
"HttpTransport",
|
|
31
|
+
"SdkError",
|
|
32
|
+
"ValidationError",
|
|
33
|
+
"NetworkError",
|
|
34
|
+
"ApiError",
|
|
35
|
+
"AuthError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"NotFoundError",
|
|
38
|
+
"ServerError",
|
|
39
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from .exceptions import ValidationError
|
|
2
|
+
from .location import location_to_params
|
|
3
|
+
from .options import ForecastOptions, validate_days, validate_lang, validate_units
|
|
4
|
+
from .transport import HttpTransport
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseClient(object):
|
|
8
|
+
def __init__(self,
|
|
9
|
+
token,
|
|
10
|
+
transport=None,
|
|
11
|
+
base_url=None,
|
|
12
|
+
timeout=None,
|
|
13
|
+
retries=None,
|
|
14
|
+
backoff_factor=None,
|
|
15
|
+
user_agent=None,
|
|
16
|
+
proxies=None,
|
|
17
|
+
verify_ssl=True,
|
|
18
|
+
session=None,
|
|
19
|
+
auth_in_query=False,
|
|
20
|
+
retry_on_429=True,
|
|
21
|
+
default_units=None,
|
|
22
|
+
default_lang=None):
|
|
23
|
+
if not token or not isinstance(token, str):
|
|
24
|
+
raise ValidationError("token must be a non-empty string")
|
|
25
|
+
self._token = token
|
|
26
|
+
self._auth_in_query = auth_in_query
|
|
27
|
+
self._default_units = default_units
|
|
28
|
+
self._default_lang = default_lang
|
|
29
|
+
|
|
30
|
+
if transport is not None:
|
|
31
|
+
self._transport = transport
|
|
32
|
+
else:
|
|
33
|
+
kwargs = {
|
|
34
|
+
"base_url": base_url,
|
|
35
|
+
"timeout": timeout,
|
|
36
|
+
"retries": retries,
|
|
37
|
+
"backoff_factor": backoff_factor,
|
|
38
|
+
"user_agent": user_agent,
|
|
39
|
+
"proxies": proxies,
|
|
40
|
+
"verify_ssl": verify_ssl,
|
|
41
|
+
"session": session,
|
|
42
|
+
"retry_on_429": retry_on_429,
|
|
43
|
+
}
|
|
44
|
+
cleaned = {}
|
|
45
|
+
for key in kwargs:
|
|
46
|
+
if kwargs[key] is not None:
|
|
47
|
+
cleaned[key] = kwargs[key]
|
|
48
|
+
self._transport = HttpTransport(**cleaned)
|
|
49
|
+
|
|
50
|
+
def _with_auth(self, params):
|
|
51
|
+
headers = {}
|
|
52
|
+
if self._auth_in_query:
|
|
53
|
+
params["key"] = self._token
|
|
54
|
+
else:
|
|
55
|
+
headers["X-token"] = self._token
|
|
56
|
+
return params, headers
|
|
57
|
+
|
|
58
|
+
def _build_params(self,
|
|
59
|
+
location,
|
|
60
|
+
options,
|
|
61
|
+
default_days=None,
|
|
62
|
+
supports_days=True,
|
|
63
|
+
supports_units=True,
|
|
64
|
+
supports_lang=True):
|
|
65
|
+
params = location_to_params(location)
|
|
66
|
+
if options is None:
|
|
67
|
+
options = ForecastOptions()
|
|
68
|
+
if not isinstance(options, ForecastOptions):
|
|
69
|
+
raise ValidationError("options must be ForecastOptions or None")
|
|
70
|
+
|
|
71
|
+
if supports_days:
|
|
72
|
+
days = options.days
|
|
73
|
+
if days is None:
|
|
74
|
+
days = default_days
|
|
75
|
+
validate_days(days)
|
|
76
|
+
if days is not None:
|
|
77
|
+
params["days"] = int(days)
|
|
78
|
+
|
|
79
|
+
if supports_units:
|
|
80
|
+
units = options.units
|
|
81
|
+
if units is None:
|
|
82
|
+
units = self._default_units
|
|
83
|
+
validate_units(units)
|
|
84
|
+
if units is not None:
|
|
85
|
+
params["unit"] = units
|
|
86
|
+
|
|
87
|
+
if supports_lang:
|
|
88
|
+
lang = options.lang
|
|
89
|
+
if lang is None:
|
|
90
|
+
lang = self._default_lang
|
|
91
|
+
validate_lang(lang)
|
|
92
|
+
if lang is not None:
|
|
93
|
+
params["lang"] = lang
|
|
94
|
+
|
|
95
|
+
return params
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ._base import BaseClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AirQualityClient(BaseClient):
|
|
5
|
+
def __init__(self, token, default_days=3, **kwargs):
|
|
6
|
+
super(AirQualityClient, self).__init__(token, **kwargs)
|
|
7
|
+
self._default_days = default_days
|
|
8
|
+
|
|
9
|
+
def by_days(self, location, options=None):
|
|
10
|
+
params = self._build_params(
|
|
11
|
+
location,
|
|
12
|
+
options,
|
|
13
|
+
default_days=self._default_days,
|
|
14
|
+
supports_days=True,
|
|
15
|
+
)
|
|
16
|
+
params, headers = self._with_auth(params)
|
|
17
|
+
return self._transport.get("/v2/air/by-days/", params=params, headers=headers)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from ._base import BaseClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WeatherClient(BaseClient):
|
|
5
|
+
def __init__(self, token, default_days=None, **kwargs):
|
|
6
|
+
super(WeatherClient, self).__init__(token, **kwargs)
|
|
7
|
+
self._default_days = self._normalize_default_days(default_days)
|
|
8
|
+
|
|
9
|
+
def current(self, location):
|
|
10
|
+
params = self._build_params(location, options=None, supports_days=False, supports_lang=False)
|
|
11
|
+
params, headers = self._with_auth(params)
|
|
12
|
+
return self._transport.get("/v2/current/", params=params, headers=headers)
|
|
13
|
+
|
|
14
|
+
def forecast_hourly(self, location, options=None):
|
|
15
|
+
params = self._build_params(
|
|
16
|
+
location,
|
|
17
|
+
options,
|
|
18
|
+
default_days=self._default_days["forecast_hourly"],
|
|
19
|
+
supports_days=True,
|
|
20
|
+
)
|
|
21
|
+
params, headers = self._with_auth(params)
|
|
22
|
+
return self._transport.get("/v2/forecast/by-hours/", params=params, headers=headers)
|
|
23
|
+
|
|
24
|
+
def forecast_3hourly(self, location, options=None):
|
|
25
|
+
params = self._build_params(
|
|
26
|
+
location,
|
|
27
|
+
options,
|
|
28
|
+
default_days=self._default_days["forecast_3hourly"],
|
|
29
|
+
supports_days=True,
|
|
30
|
+
)
|
|
31
|
+
params, headers = self._with_auth(params)
|
|
32
|
+
return self._transport.get("/v2/forecast/by-3hours/", params=params, headers=headers)
|
|
33
|
+
|
|
34
|
+
def forecast_daily(self, location, options=None):
|
|
35
|
+
params = self._build_params(
|
|
36
|
+
location,
|
|
37
|
+
options,
|
|
38
|
+
default_days=self._default_days["forecast_daily"],
|
|
39
|
+
supports_days=True,
|
|
40
|
+
)
|
|
41
|
+
params, headers = self._with_auth(params)
|
|
42
|
+
return self._transport.get("/v2/forecast/by-days/", params=params, headers=headers)
|
|
43
|
+
|
|
44
|
+
# Aliases for camelCase compatibility
|
|
45
|
+
def forecastHourly(self, location, options=None):
|
|
46
|
+
return self.forecast_hourly(location, options)
|
|
47
|
+
|
|
48
|
+
def forecast3hourly(self, location, options=None):
|
|
49
|
+
return self.forecast_3hourly(location, options)
|
|
50
|
+
|
|
51
|
+
def forecastDaily(self, location, options=None):
|
|
52
|
+
return self.forecast_daily(location, options)
|
|
53
|
+
|
|
54
|
+
def _normalize_default_days(self, default_days):
|
|
55
|
+
defaults = {
|
|
56
|
+
"forecast_hourly": 1,
|
|
57
|
+
"forecast_3hourly": 2,
|
|
58
|
+
"forecast_daily": 3,
|
|
59
|
+
}
|
|
60
|
+
if default_days is None:
|
|
61
|
+
return defaults
|
|
62
|
+
if isinstance(default_days, dict):
|
|
63
|
+
merged = defaults.copy()
|
|
64
|
+
merged.update(default_days)
|
|
65
|
+
return merged
|
|
66
|
+
try:
|
|
67
|
+
days_val = int(default_days)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
return defaults
|
|
70
|
+
return {
|
|
71
|
+
"forecast_hourly": days_val,
|
|
72
|
+
"forecast_3hourly": days_val,
|
|
73
|
+
"forecast_daily": days_val,
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class SdkError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(SdkError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NetworkError(SdkError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(SdkError):
|
|
14
|
+
def __init__(self, status_code, body, message, url, params):
|
|
15
|
+
super(ApiError, self).__init__(message)
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.body = body
|
|
18
|
+
self.message = message
|
|
19
|
+
self.url = url
|
|
20
|
+
self.params = params
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuthError(ApiError):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RateLimitError(ApiError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NotFoundError(ApiError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ServerError(ApiError):
|
|
36
|
+
pass
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from ._base import BaseClient
|
|
2
|
+
from .exceptions import ValidationError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GeographyClient(BaseClient):
|
|
6
|
+
def countries(self):
|
|
7
|
+
params, headers = self._with_auth({})
|
|
8
|
+
return self._transport.get("/v2/geography/countries/", params=params, headers=headers)
|
|
9
|
+
|
|
10
|
+
def country_by_code(self, country_code):
|
|
11
|
+
if not isinstance(country_code, str) or not country_code:
|
|
12
|
+
raise ValidationError("country_code must be a non-empty string")
|
|
13
|
+
params = {"country_code": country_code}
|
|
14
|
+
params, headers = self._with_auth(params)
|
|
15
|
+
return self._transport.get("/v2/geography/countries/", params=params, headers=headers)
|
|
16
|
+
|
|
17
|
+
def cities_by_country(self, country_code):
|
|
18
|
+
if not isinstance(country_code, str) or not country_code:
|
|
19
|
+
raise ValidationError("country_code must be a non-empty string")
|
|
20
|
+
params = {"country_code": country_code}
|
|
21
|
+
params, headers = self._with_auth(params)
|
|
22
|
+
return self._transport.get("/v2/geography/countries/cities/", params=params, headers=headers)
|
|
23
|
+
|
|
24
|
+
def search(self, q, limit=None):
|
|
25
|
+
if not isinstance(q, str) or not q:
|
|
26
|
+
raise ValidationError("q must be a non-empty string")
|
|
27
|
+
params = {"q": q}
|
|
28
|
+
if limit is not None:
|
|
29
|
+
try:
|
|
30
|
+
limit_val = int(limit)
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
raise ValidationError("limit must be an integer")
|
|
33
|
+
if limit_val < 1:
|
|
34
|
+
raise ValidationError("limit must be >= 1")
|
|
35
|
+
params["limit"] = limit_val
|
|
36
|
+
params, headers = self._with_auth(params)
|
|
37
|
+
return self._transport.get("/v2/geography/search/", params=params, headers=headers)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from ._base import BaseClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GeomagneticClient(BaseClient):
|
|
5
|
+
def by_days(self, location):
|
|
6
|
+
params = self._build_params(location, options=None, supports_days=False, supports_units=False, supports_lang=False)
|
|
7
|
+
params, headers = self._with_auth(params)
|
|
8
|
+
return self._transport.get("/v2/geomagnetic/by-days/", params=params, headers=headers)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
|
|
3
|
+
from .exceptions import ValidationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LocationSlug(object):
|
|
7
|
+
def __init__(self, slug):
|
|
8
|
+
if not isinstance(slug, str) or not slug:
|
|
9
|
+
raise ValidationError("slug must be a non-empty string")
|
|
10
|
+
self.slug = slug
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LocationCoords(object):
|
|
14
|
+
def __init__(self, lat, lon):
|
|
15
|
+
try:
|
|
16
|
+
lat_val = float(lat)
|
|
17
|
+
lon_val = float(lon)
|
|
18
|
+
except (TypeError, ValueError):
|
|
19
|
+
raise ValidationError("lat and lon must be numbers")
|
|
20
|
+
if lat_val < -90 or lat_val > 90:
|
|
21
|
+
raise ValidationError("lat must be between -90 and 90")
|
|
22
|
+
if lon_val < -180 or lon_val > 180:
|
|
23
|
+
raise ValidationError("lon must be between -180 and 180")
|
|
24
|
+
self.lat = lat_val
|
|
25
|
+
self.lon = lon_val
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LocationIP(object):
|
|
29
|
+
def __init__(self, ip):
|
|
30
|
+
if not isinstance(ip, str) or not ip:
|
|
31
|
+
raise ValidationError("ip must be a non-empty string")
|
|
32
|
+
try:
|
|
33
|
+
ipaddress.ip_address(ip)
|
|
34
|
+
except ValueError:
|
|
35
|
+
raise ValidationError("ip must be a valid IPv4 or IPv6 string")
|
|
36
|
+
self.ip = ip
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def location_to_params(location):
|
|
40
|
+
if isinstance(location, LocationSlug):
|
|
41
|
+
return {"slug": location.slug}
|
|
42
|
+
if isinstance(location, LocationCoords):
|
|
43
|
+
return {"lat": location.lat, "lon": location.lon}
|
|
44
|
+
if isinstance(location, LocationIP):
|
|
45
|
+
return {"ip": location.ip}
|
|
46
|
+
raise ValidationError("location must be LocationSlug, LocationCoords, or LocationIP")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .exceptions import ValidationError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
UNITS_METRIC = "metric"
|
|
5
|
+
UNITS_IMPERIAL = "imperial"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ForecastOptions(object):
|
|
9
|
+
def __init__(self, days=None, units=None, lang=None):
|
|
10
|
+
self.days = days
|
|
11
|
+
self.units = units
|
|
12
|
+
self.lang = lang
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_days(days):
|
|
16
|
+
if days is None:
|
|
17
|
+
return
|
|
18
|
+
try:
|
|
19
|
+
days_val = int(days)
|
|
20
|
+
except (TypeError, ValueError):
|
|
21
|
+
raise ValidationError("days must be an integer")
|
|
22
|
+
if days_val < 1:
|
|
23
|
+
raise ValidationError("days must be >= 1")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_units(units):
|
|
27
|
+
if units is None:
|
|
28
|
+
return
|
|
29
|
+
if units not in (UNITS_METRIC, UNITS_IMPERIAL):
|
|
30
|
+
raise ValidationError("units must be 'metric' or 'imperial'")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_lang(lang):
|
|
34
|
+
if lang is None:
|
|
35
|
+
return
|
|
36
|
+
if not isinstance(lang, str) or not lang:
|
|
37
|
+
raise ValidationError("lang must be a non-empty string")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from .exceptions import ApiError, AuthError, NetworkError, NotFoundError, RateLimitError, ServerError
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://api.meteoflow.com"
|
|
8
|
+
DEFAULT_TIMEOUT = 10
|
|
9
|
+
DEFAULT_RETRIES = 2
|
|
10
|
+
DEFAULT_BACKOFF = 0.5
|
|
11
|
+
DEFAULT_USER_AGENT = "meteoflow-python/0.1.0"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HttpTransport(object):
|
|
15
|
+
def __init__(self,
|
|
16
|
+
base_url=DEFAULT_BASE_URL,
|
|
17
|
+
timeout=DEFAULT_TIMEOUT,
|
|
18
|
+
retries=DEFAULT_RETRIES,
|
|
19
|
+
backoff_factor=DEFAULT_BACKOFF,
|
|
20
|
+
user_agent=None,
|
|
21
|
+
proxies=None,
|
|
22
|
+
verify_ssl=True,
|
|
23
|
+
session=None,
|
|
24
|
+
retry_on_429=True):
|
|
25
|
+
self.base_url = base_url.rstrip("/")
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
self.retries = retries
|
|
28
|
+
self.backoff_factor = backoff_factor
|
|
29
|
+
self.user_agent = user_agent or DEFAULT_USER_AGENT
|
|
30
|
+
self.proxies = proxies
|
|
31
|
+
self.verify_ssl = verify_ssl
|
|
32
|
+
self.retry_on_429 = retry_on_429
|
|
33
|
+
self.session = session or requests.Session()
|
|
34
|
+
|
|
35
|
+
def get(self, path, params=None, headers=None):
|
|
36
|
+
return self.request("GET", path, params=params, headers=headers)
|
|
37
|
+
|
|
38
|
+
def request(self, method, path, params=None, headers=None):
|
|
39
|
+
url = self.base_url + path
|
|
40
|
+
request_headers = {
|
|
41
|
+
"User-Agent": self.user_agent,
|
|
42
|
+
}
|
|
43
|
+
if headers:
|
|
44
|
+
request_headers.update(headers)
|
|
45
|
+
|
|
46
|
+
attempt = 0
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
response = self.session.request(
|
|
50
|
+
method,
|
|
51
|
+
url,
|
|
52
|
+
params=params,
|
|
53
|
+
headers=request_headers,
|
|
54
|
+
timeout=self.timeout,
|
|
55
|
+
proxies=self.proxies,
|
|
56
|
+
verify=self.verify_ssl,
|
|
57
|
+
)
|
|
58
|
+
except requests.RequestException as exc:
|
|
59
|
+
if attempt >= self.retries:
|
|
60
|
+
raise NetworkError(str(exc))
|
|
61
|
+
self._sleep_backoff(attempt)
|
|
62
|
+
attempt += 1
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if self._should_retry(response, method) and attempt < self.retries:
|
|
66
|
+
self._sleep_backoff(attempt)
|
|
67
|
+
attempt += 1
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
71
|
+
self._raise_api_error(response, params)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
return response.json()
|
|
75
|
+
except ValueError:
|
|
76
|
+
raise ApiError(
|
|
77
|
+
response.status_code,
|
|
78
|
+
response.text,
|
|
79
|
+
"Invalid JSON response",
|
|
80
|
+
response.url,
|
|
81
|
+
params,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _sleep_backoff(self, attempt):
|
|
85
|
+
if self.backoff_factor is None:
|
|
86
|
+
return
|
|
87
|
+
delay = self.backoff_factor * (2 ** attempt)
|
|
88
|
+
if delay > 0:
|
|
89
|
+
time.sleep(delay)
|
|
90
|
+
|
|
91
|
+
def _should_retry(self, response, method):
|
|
92
|
+
if method != "GET":
|
|
93
|
+
return False
|
|
94
|
+
if response.status_code >= 500:
|
|
95
|
+
return True
|
|
96
|
+
if response.status_code == 429 and self.retry_on_429:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def _raise_api_error(self, response, params):
|
|
101
|
+
status = response.status_code
|
|
102
|
+
message = self._extract_message(response)
|
|
103
|
+
body = response.text
|
|
104
|
+
url = response.url
|
|
105
|
+
if status in (401, 403):
|
|
106
|
+
raise AuthError(status, body, message, url, params)
|
|
107
|
+
if status == 404:
|
|
108
|
+
raise NotFoundError(status, body, message, url, params)
|
|
109
|
+
if status == 429:
|
|
110
|
+
raise RateLimitError(status, body, message, url, params)
|
|
111
|
+
if status >= 500:
|
|
112
|
+
raise ServerError(status, body, message, url, params)
|
|
113
|
+
raise ApiError(status, body, message, url, params)
|
|
114
|
+
|
|
115
|
+
def _extract_message(self, response):
|
|
116
|
+
try:
|
|
117
|
+
data = response.json()
|
|
118
|
+
except ValueError:
|
|
119
|
+
return response.text
|
|
120
|
+
if isinstance(data, dict):
|
|
121
|
+
for key in ("message", "error", "detail"):
|
|
122
|
+
if key in data and data[key]:
|
|
123
|
+
return data[key]
|
|
124
|
+
return response.text
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meteoflow
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for Meteoflow Weather API
|
|
5
|
+
Author: Meteoflow
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.4
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.5
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Requires-Python: >=3.4
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: requests>=2.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
Dynamic: requires-python
|
|
20
|
+
|
|
21
|
+
# Meteoflow Python SDK
|
|
22
|
+
|
|
23
|
+
Python SDK for Meteoflow Weather API.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install meteoflow
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from meteoflow import WeatherClient, LocationSlug, LocationCoords, LocationIP
|
|
35
|
+
|
|
36
|
+
client = WeatherClient("TOKEN")
|
|
37
|
+
|
|
38
|
+
# Current weather by slug
|
|
39
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
40
|
+
|
|
41
|
+
# Current weather by coords
|
|
42
|
+
current = client.current(LocationCoords(51.5072, -0.1275))
|
|
43
|
+
|
|
44
|
+
# Current weather by IP
|
|
45
|
+
current = client.current(LocationIP("8.8.8.8"))
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Forecasts
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from meteoflow import WeatherClient, LocationIP, ForecastOptions
|
|
52
|
+
|
|
53
|
+
client = WeatherClient("TOKEN")
|
|
54
|
+
|
|
55
|
+
# Hourly forecast by IP
|
|
56
|
+
options = ForecastOptions(days=1, units="metric", lang="en")
|
|
57
|
+
forecast = client.forecast_hourly(LocationIP("8.8.8.8"), options)
|
|
58
|
+
|
|
59
|
+
# 3-hour forecast by IP
|
|
60
|
+
options = ForecastOptions(days=2, units="metric", lang="en")
|
|
61
|
+
forecast = client.forecast_3hourly(LocationIP("8.8.8.8"), options)
|
|
62
|
+
|
|
63
|
+
# Daily forecast by IP
|
|
64
|
+
options = ForecastOptions(days=5, units="metric", lang="en")
|
|
65
|
+
forecast = client.forecast_daily(LocationIP("8.8.8.8"), options)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Air Quality
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from meteoflow import AirQualityClient, LocationCoords, ForecastOptions
|
|
72
|
+
|
|
73
|
+
client = AirQualityClient("TOKEN")
|
|
74
|
+
|
|
75
|
+
air = client.by_days(LocationCoords(51.5072, -0.1275), ForecastOptions(days=3))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Geomagnetic
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from meteoflow import GeomagneticClient, LocationSlug
|
|
82
|
+
|
|
83
|
+
client = GeomagneticClient("TOKEN")
|
|
84
|
+
|
|
85
|
+
geo = client.by_days(LocationSlug("united-kingdom-london"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Geography Search → Weather
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from meteoflow import GeographyClient, WeatherClient, LocationSlug
|
|
92
|
+
|
|
93
|
+
geo = GeographyClient("TOKEN")
|
|
94
|
+
weather = WeatherClient("TOKEN")
|
|
95
|
+
|
|
96
|
+
results = geo.search("London", limit=1)
|
|
97
|
+
slug = results["items"][0]["slug"]
|
|
98
|
+
|
|
99
|
+
current = weather.current(LocationSlug(slug))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Auth: Header vs Query
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from meteoflow import WeatherClient, LocationSlug
|
|
106
|
+
|
|
107
|
+
# Default: X-token header
|
|
108
|
+
client = WeatherClient("TOKEN")
|
|
109
|
+
current = client.current(LocationSlug("united-kingdom-london"))
|
|
110
|
+
|
|
111
|
+
# Query auth: ?key=TOKEN
|
|
112
|
+
client_q = WeatherClient("TOKEN", auth_in_query=True)
|
|
113
|
+
current_q = client_q.current(LocationSlug("united-kingdom-london"))
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Notes
|
|
117
|
+
|
|
118
|
+
- Supported Python versions: 3.4+
|
|
119
|
+
- Sync only in core package
|
|
120
|
+
- Returns parsed JSON as `dict`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
meteoflow/__init__.py
|
|
6
|
+
meteoflow/_base.py
|
|
7
|
+
meteoflow/air.py
|
|
8
|
+
meteoflow/client.py
|
|
9
|
+
meteoflow/exceptions.py
|
|
10
|
+
meteoflow/geography.py
|
|
11
|
+
meteoflow/geomagnetic.py
|
|
12
|
+
meteoflow/location.py
|
|
13
|
+
meteoflow/options.py
|
|
14
|
+
meteoflow/transport.py
|
|
15
|
+
meteoflow.egg-info/PKG-INFO
|
|
16
|
+
meteoflow.egg-info/SOURCES.txt
|
|
17
|
+
meteoflow.egg-info/dependency_links.txt
|
|
18
|
+
meteoflow.egg-info/requires.txt
|
|
19
|
+
meteoflow.egg-info/top_level.txt
|
|
20
|
+
tests/test_location.py
|
|
21
|
+
tests/test_options.py
|
|
22
|
+
tests/test_query.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
meteoflow
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "meteoflow"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python SDK for Meteoflow Weather API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.4"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Meteoflow" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"requests>=2.0"
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.4",
|
|
21
|
+
"Programming Language :: Python :: 3.5",
|
|
22
|
+
"Programming Language :: Python :: 3.6",
|
|
23
|
+
"Programming Language :: Python :: 3.7",
|
|
24
|
+
"Programming Language :: Python :: 3.8",
|
|
25
|
+
"Programming Language :: Python :: 3.9"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["meteoflow"]
|
meteoflow-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="meteoflow",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
description="Python SDK for Meteoflow Weather API",
|
|
7
|
+
long_description=open("README.md").read(),
|
|
8
|
+
long_description_content_type="text/markdown",
|
|
9
|
+
author="Meteoflow",
|
|
10
|
+
license="MIT",
|
|
11
|
+
packages=find_packages(exclude=("tests",)),
|
|
12
|
+
install_requires=[
|
|
13
|
+
"requests>=2.0",
|
|
14
|
+
],
|
|
15
|
+
python_requires=">=3.4",
|
|
16
|
+
classifiers=[
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.4",
|
|
19
|
+
"Programming Language :: Python :: 3.5",
|
|
20
|
+
"Programming Language :: Python :: 3.6",
|
|
21
|
+
"Programming Language :: Python :: 3.7",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
],
|
|
25
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from meteoflow.exceptions import ValidationError
|
|
4
|
+
from meteoflow.location import LocationCoords, LocationIP, LocationSlug, location_to_params
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_location_slug_params():
|
|
8
|
+
loc = LocationSlug("paris")
|
|
9
|
+
assert location_to_params(loc) == {"slug": "paris"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_location_coords_params():
|
|
13
|
+
loc = LocationCoords(10.5, -20.25)
|
|
14
|
+
assert location_to_params(loc) == {"lat": 10.5, "lon": -20.25}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_location_ip_params():
|
|
18
|
+
loc = LocationIP("8.8.8.8")
|
|
19
|
+
assert location_to_params(loc) == {"ip": "8.8.8.8"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_location_invalid_slug():
|
|
23
|
+
with pytest.raises(ValidationError):
|
|
24
|
+
LocationSlug("")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_location_invalid_coords():
|
|
28
|
+
with pytest.raises(ValidationError):
|
|
29
|
+
LocationCoords(91, 0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_location_invalid_ip():
|
|
33
|
+
with pytest.raises(ValidationError):
|
|
34
|
+
LocationIP("999.999.999.999")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from meteoflow.exceptions import ValidationError
|
|
4
|
+
from meteoflow.options import validate_days, validate_lang, validate_units
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_validate_days_ok():
|
|
8
|
+
validate_days(1)
|
|
9
|
+
validate_days("2")
|
|
10
|
+
validate_days(None)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_validate_days_invalid():
|
|
14
|
+
with pytest.raises(ValidationError):
|
|
15
|
+
validate_days(0)
|
|
16
|
+
with pytest.raises(ValidationError):
|
|
17
|
+
validate_days("bad")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_validate_units_ok():
|
|
21
|
+
validate_units("metric")
|
|
22
|
+
validate_units("imperial")
|
|
23
|
+
validate_units(None)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_validate_units_invalid():
|
|
27
|
+
with pytest.raises(ValidationError):
|
|
28
|
+
validate_units("si")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_validate_lang_ok():
|
|
32
|
+
validate_lang("en")
|
|
33
|
+
validate_lang(None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_validate_lang_invalid():
|
|
37
|
+
with pytest.raises(ValidationError):
|
|
38
|
+
validate_lang(123)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from meteoflow.client import WeatherClient
|
|
2
|
+
from meteoflow.location import LocationCoords, LocationSlug
|
|
3
|
+
from meteoflow.options import ForecastOptions
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FakeTransport(object):
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.last = None
|
|
9
|
+
|
|
10
|
+
def get(self, path, params=None, headers=None):
|
|
11
|
+
self.last = {
|
|
12
|
+
"path": path,
|
|
13
|
+
"params": params,
|
|
14
|
+
"headers": headers,
|
|
15
|
+
}
|
|
16
|
+
return {"ok": True}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_weather_current_slug_auth_header():
|
|
20
|
+
transport = FakeTransport()
|
|
21
|
+
client = WeatherClient("TOKEN", transport=transport)
|
|
22
|
+
client.current(LocationSlug("london"))
|
|
23
|
+
assert transport.last["path"] == "/v2/current/"
|
|
24
|
+
assert transport.last["params"] == {"slug": "london"}
|
|
25
|
+
assert transport.last["headers"]["X-token"] == "TOKEN"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_weather_forecast_coords_options():
|
|
29
|
+
transport = FakeTransport()
|
|
30
|
+
client = WeatherClient("TOKEN", transport=transport, default_units="metric", default_lang="en")
|
|
31
|
+
options = ForecastOptions(days=2, units="imperial", lang="ru")
|
|
32
|
+
client.forecast_daily(LocationCoords(10, 20), options)
|
|
33
|
+
assert transport.last["path"] == "/v2/forecast/by-days/"
|
|
34
|
+
assert transport.last["params"]["lat"] == 10.0
|
|
35
|
+
assert transport.last["params"]["lon"] == 20.0
|
|
36
|
+
assert transport.last["params"]["days"] == 2
|
|
37
|
+
assert transport.last["params"]["unit"] == "imperial"
|
|
38
|
+
assert transport.last["params"]["lang"] == "ru"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_weather_auth_query():
|
|
42
|
+
transport = FakeTransport()
|
|
43
|
+
client = WeatherClient("TOKEN", transport=transport, auth_in_query=True)
|
|
44
|
+
client.current(LocationSlug("london"))
|
|
45
|
+
assert transport.last["params"]["key"] == "TOKEN"
|