python-meteolux 0.1.0__py3-none-any.whl

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.
meteolux/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Module entry point."""
2
+
3
+ from .async_api import AsyncMeteoLuxClient
4
+
5
+ __all__ = ['AsyncMeteoLuxClient']
meteolux/async_api.py ADDED
@@ -0,0 +1,252 @@
1
+ """A Python client for the MeteoLux API."""
2
+ import typing
3
+ from typing import Any, Optional
4
+
5
+ import httpx
6
+
7
+ from .exceptions import NotFoundError
8
+ from .models import (
9
+ ATCReport,
10
+ Bookmarks,
11
+ InObservation,
12
+ ObservationMetadataResponse,
13
+ ObservationResponse,
14
+ User,
15
+ WeatherResponse,
16
+ )
17
+
18
+
19
+ class AsyncMeteoLuxClient:
20
+ """A Python client for the MeteoLux API, built with httpx and Pydantic.
21
+
22
+ This client is generated from the OpenAPI specification and provides
23
+ methods for all available endpoints, returning structured Pydantic models.
24
+ """
25
+
26
+ def __init__(self, base_url: str = 'https://metapi.ana.lu/api/v1') -> None:
27
+ """Initializes the client with the base URL.
28
+
29
+ Args:
30
+ base_url (str): The base URL for the API.
31
+ """
32
+ self.base_url = base_url
33
+ self.client = httpx.AsyncClient(base_url=self.base_url, timeout=10.0)
34
+
35
+ async def _request(self, method: str, endpoint: str, response_model: Optional[Any] = None, **kwargs: Any) -> Any:
36
+ """Internal method to handle all API requests and common error handling.
37
+
38
+ Args:
39
+ method (str): The HTTP method (e.g., "GET", "POST").
40
+ endpoint (str): The API endpoint path.
41
+ response_model (Optional[Any]): The Pydantic model to use for response parsing.
42
+ **kwargs: Additional arguments for the httpx request (e.g., params, json).
43
+
44
+ Returns:
45
+ Any: The Pydantic model instance or raw JSON data.
46
+
47
+ Raises:
48
+ NotFoundError: If the API returns a 404 Not Found status code.
49
+ httpx.HTTPStatusError: If the response status code is another error.
50
+ httpx.RequestError: For network-related issues.
51
+ """
52
+ try:
53
+ response = await self.client.request(method, endpoint, **kwargs)
54
+ response.raise_for_status()
55
+
56
+ if response.status_code == 204:
57
+ return None
58
+
59
+ data = response.json()
60
+ if response_model:
61
+ return response_model.model_validate(data)
62
+ return data
63
+
64
+ except httpx.HTTPStatusError as exc:
65
+ if exc.response.status_code == 404:
66
+ raise NotFoundError(detail=exc.response.text) from exc
67
+
68
+ raise
69
+ except httpx.RequestError:
70
+ raise
71
+
72
+ # --- ATC Endpoints ---
73
+
74
+ async def get_atc_report(self) -> ATCReport:
75
+ """Get data for the ATC dashboard.
76
+
77
+ Corresponds to GET /atc/report.
78
+
79
+ Returns:
80
+ ATCReport: An ATCReport Pydantic model instance.
81
+ """
82
+ endpoint = '/atc/report'
83
+ return await self._request('GET', endpoint, response_model=ATCReport)
84
+
85
+ # --- HVD Endpoints ---
86
+
87
+ async def get_observations_hvd(self) -> ObservationResponse:
88
+ """Return last minute observation data.
89
+
90
+ Corresponds to GET /hvd/observations.
91
+
92
+ Returns:
93
+ ObservationResponse: An ObservationResponse Pydantic model instance.
94
+ """
95
+ endpoint = '/hvd/observations'
96
+ return await self._request('GET', endpoint, response_model=ObservationResponse)
97
+
98
+ async def get_observations_metadata_hvd(self) -> ObservationMetadataResponse:
99
+ """Return observations metadata.
100
+
101
+ Corresponds to GET /hvd/observations/metadata.
102
+
103
+ Returns:
104
+ ObservationMetadataResponse: An ObservationMetadataResponse Pydantic model instance.
105
+ """
106
+ endpoint = '/hvd/observations/metadata'
107
+ return await self._request('GET', endpoint, response_model=ObservationMetadataResponse)
108
+
109
+ async def get_station_information_hvd(self, station_id: str) -> list[Any]:
110
+ """Return station information, by ID.
111
+
112
+ Corresponds to GET /hvd/stations/{station_id}.
113
+
114
+ Args:
115
+ station_id (str): The ID of the station to retrieve.
116
+
117
+ Returns:
118
+ Any: Station data from the response. The OpenAPI spec indicates the return type is a list of Stations.
119
+ """
120
+ endpoint = f'/hvd/stations/{station_id}'
121
+ return await self._request('GET', endpoint)
122
+
123
+ async def get_all_station_information_hvd(self) -> list[Any]:
124
+ """Return all station information.
125
+
126
+ Corresponds to GET /hvd/stations.
127
+
128
+ Returns:
129
+ list[Any]: A list of station objects. The OpenAPI spec indicates the return type is a list of Stations.
130
+ """
131
+ endpoint = '/hvd/stations'
132
+ return await self._request('GET', endpoint)
133
+
134
+ # --- MetApp Endpoints ---
135
+
136
+ async def get_weather(self, langcode: str = 'fr', city: Optional[int] = None, lat: Optional[float] = None, long: Optional[float] = None) -> WeatherResponse:
137
+ """Get weather for a city/language or lat/long.
138
+
139
+ Corresponds to GET /metapp/weather.
140
+
141
+ Args:
142
+ langcode (str): The language code (fr, de, en, lb).
143
+ city (Optional[int]): The city ID.
144
+ lat (Optional[float]): Latitude.
145
+ long (Optional[float]): Longitude.
146
+
147
+ Returns:
148
+ WeatherResponse: A WeatherResponse Pydantic model instance.
149
+ """
150
+ endpoint = '/metapp/weather'
151
+ params: dict[str, str] = {'langcode': langcode}
152
+ if city is not None:
153
+ params['city'] = str(city)
154
+ if lat is not None:
155
+ params['lat'] = str(lat)
156
+ if long is not None:
157
+ params['long'] = str(long)
158
+ return await self._request('GET', endpoint, params=params, response_model=WeatherResponse)
159
+
160
+ async def update_user(self, user_data: User) -> None:
161
+ """Add or update a user's token and preferences.
162
+
163
+ Corresponds to POST /metapp/user.
164
+
165
+ Args:
166
+ user_data (User): A Pydantic User model instance.
167
+ """
168
+ endpoint = '/metapp/user'
169
+ await self._request('POST', endpoint, json=user_data.model_dump(by_alias=True))
170
+
171
+ async def get_bookmarks(self, langcode: str = 'fr', lat: Optional[float] = None, long: Optional[float] = None) -> Bookmarks:
172
+ """Return all cities and the closest one if lat/long are given.
173
+
174
+ Corresponds to GET /metapp/bookmarks.
175
+
176
+ Args:
177
+ langcode (str): The language code (fr, de, en, lb).
178
+ lat (Optional[float]): Latitude.
179
+ long (Optional[float]): Longitude.
180
+
181
+ Returns:
182
+ Bookmarks: A Bookmarks Pydantic model instance.
183
+ """
184
+ endpoint = '/metapp/bookmarks'
185
+ params: dict[str, str] = {'langcode': langcode}
186
+ if lat is not None:
187
+ params['lat'] = str(lat)
188
+ if long is not None:
189
+ params['long'] = str(long)
190
+ return await self._request('GET', endpoint, params=params, response_model=Bookmarks)
191
+
192
+ async def get_interface_texts(self, lang: typing.Literal['fr', 'de', 'en', 'lb'] = 'fr') -> dict[str, Any]:
193
+ """Return translated interface texts for the mobile app.
194
+
195
+ Note: The spec for this endpoint's response is an un-typed object.
196
+
197
+ Corresponds to GET /metapp/text.
198
+
199
+ Args:
200
+ lang (str): The language code (fr, de, en, lb).
201
+
202
+ Returns:
203
+ dict[str, Any]: A dictionary with translated strings.
204
+ """
205
+ endpoint = '/metapp/text'
206
+ params = {'lang': lang}
207
+ return await self._request('GET', endpoint, params=params)
208
+
209
+ async def stream_image(self, filename: str) -> httpx.Response:
210
+ """Stream an image from the cluster.
211
+
212
+ Corresponds to GET /metapp/image/{filename}.
213
+
214
+ Args:
215
+ filename (str): The name of the image file.
216
+
217
+ Returns:
218
+ httpx.Response: The raw httpx Response object to handle streaming.
219
+ """
220
+ endpoint = f'/metapp/image/{filename}'
221
+ return await self.client.get(endpoint, timeout=10.0)
222
+
223
+ async def get_observations_metapp(self) -> list[Any]:
224
+ """Return participative observations in the last 30 minutes.
225
+
226
+ Note: The spec for this endpoint's response is an array of untyped objects.
227
+
228
+ Corresponds to GET /metapp/observations.
229
+
230
+ Returns:
231
+ list[Any]: A list of objects.
232
+ """
233
+ endpoint = '/metapp/observations'
234
+ return await self._request('GET', endpoint)
235
+
236
+ async def add_observation(self, observation_data: InObservation) -> str:
237
+ """Add a new observation.
238
+
239
+ Corresponds to POST /metapp/observation.
240
+
241
+ Args:
242
+ observation_data (InObservation): An InObservation Pydantic model instance.
243
+
244
+ Returns:
245
+ str: The successful response message.
246
+ """
247
+ endpoint = '/metapp/observation'
248
+ return await self._request('POST', endpoint, json=observation_data.model_dump())
249
+
250
+ async def close(self) -> None:
251
+ """Closes the httpx client."""
252
+ await self.client.aclose()
meteolux/exceptions.py ADDED
@@ -0,0 +1,59 @@
1
+ """Custom exceptions for the MeteoLux API client."""
2
+
3
+
4
+ class MeteoLuxError(Exception):
5
+ """Base exception for the MeteoLux API client."""
6
+
7
+
8
+ class NotFoundError(MeteoLuxError):
9
+ """Raised when the API returns a 404 Not Found status."""
10
+
11
+ def __init__(self, detail: str) -> None:
12
+ """Initializes the NotFoundError with a detail message.
13
+
14
+ Args:
15
+ detail (str): The error message from the API.
16
+ """
17
+ self.detail = detail
18
+ super().__init__(self.detail)
19
+
20
+
21
+ class ValidationError(MeteoLuxError):
22
+ """Raised when there is a validation error in the Pydantic models."""
23
+
24
+ def __init__(self, detail: str) -> None:
25
+ """Initializes the ValidationError with a detail message.
26
+
27
+ Args:
28
+ detail (str): The error message from the API.
29
+ """
30
+ self.detail = detail
31
+ super().__init__(self.detail)
32
+
33
+
34
+ class HTTPValidationError(MeteoLuxError):
35
+ """Raised when the API returns a 422 Unprocessable Entity status."""
36
+
37
+ def __init__(self, detail: str) -> None:
38
+ """Initializes the HTTPValidationError with a detail message.
39
+
40
+ Args:
41
+ detail (str): The error message from the API.
42
+ """
43
+ self.detail = detail
44
+ super().__init__(self.detail)
45
+
46
+
47
+ class HTTPError(MeteoLuxError):
48
+ """A generic HTTP error."""
49
+
50
+ def __init__(self, status_code: int, detail: str) -> None:
51
+ """Initializes the HTTPError with a status code and detail message.
52
+
53
+ Args:
54
+ status_code (int): The HTTP status code.
55
+ detail (str): The error message from the API.
56
+ """
57
+ self.status_code = status_code
58
+ self.detail = detail
59
+ super().__init__(f'HTTP Error {self.status_code}: {self.detail}')
meteolux/models.py ADDED
@@ -0,0 +1,331 @@
1
+ """API models."""
2
+
3
+ from datetime import date, datetime
4
+ from typing import Literal, Optional, Union
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class Icon(BaseModel):
10
+ """Base icon."""
11
+
12
+ id: int
13
+ name: str
14
+
15
+
16
+ class Wind(BaseModel):
17
+ """Wind model."""
18
+
19
+ direction: str
20
+ speed: str
21
+ gusts: Optional[str] = None
22
+
23
+
24
+ class Temperature(BaseModel):
25
+ """Temperature model."""
26
+
27
+ temperature: Union[int, list[int]]
28
+ humidex: Optional[str] = None
29
+ felt: Optional[int] = None
30
+
31
+
32
+ class CurrentWeather(BaseModel):
33
+ """Part of the global weather model."""
34
+
35
+ date: datetime
36
+ icon: Icon
37
+ wind: Wind
38
+ rain: str
39
+ snow: str
40
+ type: Literal['current'] = 'current'
41
+ temperature: Temperature
42
+
43
+
44
+ class DailyWeather(BaseModel):
45
+ """For the list of following days."""
46
+
47
+ date: datetime
48
+ icon: Icon
49
+ wind: Wind
50
+ rain: str
51
+ snow: str
52
+ type: Literal['daily'] = 'daily'
53
+ temperature_min: Temperature = Field(..., alias='temperatureMin')
54
+ temperature_max: Temperature = Field(..., alias='temperatureMax')
55
+ sunshine: int
56
+ uv_index: int = Field(..., alias='uvIndex')
57
+
58
+
59
+ class HourlyWeather(BaseModel):
60
+ """For the list of following hours."""
61
+
62
+ date: datetime
63
+ icon: Icon
64
+ wind: Wind
65
+ rain: str
66
+ snow: str
67
+ type: Literal['hourly'] = 'hourly'
68
+ temperature: Temperature
69
+
70
+
71
+ class Trend(BaseModel):
72
+ """Forecast part of data."""
73
+
74
+ date: date
75
+ min_temp: float = Field(..., alias='minTemp')
76
+ max_temp: float = Field(..., alias='maxTemp')
77
+ precipitation: float
78
+
79
+
80
+ class Climatology(BaseModel):
81
+ """History part of data."""
82
+
83
+ date: datetime
84
+ min_temp: float = Field(..., alias='minTemp')
85
+ max_temp: float = Field(..., alias='maxTemp')
86
+ precipitation: float
87
+ mean_temp: float = Field(..., alias='meanTemp')
88
+ sunshine: Optional[float] = None
89
+
90
+
91
+ class GraphicalData(BaseModel):
92
+ """Graphical group of data."""
93
+
94
+ history: list[Climatology]
95
+ forecast: list[Trend]
96
+
97
+
98
+ class Vigilance(BaseModel):
99
+ """Vigilance model."""
100
+
101
+ datetime_start: datetime = Field(..., alias='datetimeStart')
102
+ datetime_end: datetime = Field(..., alias='datetimeEnd')
103
+ level: Literal[2, 3, 4]
104
+ type: int
105
+ group: int
106
+ region: Literal['north', 'south', 'all']
107
+ description: str
108
+
109
+
110
+ class RoadStatusItem(BaseModel):
111
+ """Road status item model as per the spec."""
112
+
113
+ date: Union[date, list[str]]
114
+ description: str
115
+
116
+
117
+ class ImageOut(BaseModel):
118
+ """Image with url."""
119
+
120
+ date: datetime
121
+ provider: str
122
+ url: str = Field(..., max_length=2083, min_length=1)
123
+
124
+
125
+ class Radar(BaseModel):
126
+ """Radar image model."""
127
+
128
+ real_time: list[ImageOut] = Field(..., alias='realTime')
129
+ forecast: list[ImageOut]
130
+
131
+
132
+ class Satellite(BaseModel):
133
+ """Satellite image model."""
134
+
135
+ infrared: list[ImageOut]
136
+ visual: list[ImageOut]
137
+
138
+
139
+ class MoonIcon(BaseModel):
140
+ """As different id are used."""
141
+
142
+ id: str
143
+ name: str
144
+
145
+
146
+ class Ephemeris(BaseModel):
147
+ """Ephemeris model."""
148
+
149
+ date: date
150
+ sunrise: str
151
+ sunset: str
152
+ moonrise: str
153
+ moonset: str
154
+ sunshine: str
155
+ moon_icon: MoonIcon = Field(..., alias='moonIcon')
156
+ uv_index: int = Field(..., alias='uvIndex', ge=0.0, le=12.0)
157
+
158
+
159
+ class ATCReportForecast(BaseModel):
160
+ """Forecast for ATC dashboard."""
161
+
162
+ hourly: list['HourlyWindForecast']
163
+
164
+
165
+ class ATCReport(BaseModel):
166
+ """Data for ATC dashboard."""
167
+
168
+ forecast: ATCReportForecast
169
+
170
+
171
+ class HourlyWindForecast(BaseModel):
172
+ """Hourly wind report, at different altitude (feet)."""
173
+
174
+ date: datetime
175
+ qnh: int
176
+ wind: Wind
177
+ wind1500: Wind
178
+ wind2500: Wind
179
+ wind5000: Wind
180
+ wind10000: Wind
181
+
182
+
183
+ class BookmarkCity(BaseModel):
184
+ """With additional info for mobile app ep."""
185
+
186
+ id: int
187
+ name: str
188
+ region: Literal['N', 'S'] = 'S'
189
+ canton: Literal[
190
+ 'Capellen', 'Clervaux', 'Diekirch', 'Echternach', 'Esch-sur-Alzette', 'Grevenmacher', 'Luxembourg', 'Mersch', 'Redange', 'Remich', 'Vianden', 'Wiltz'
191
+ ]
192
+ domain: Literal['villes', 'lieu', 'fluvial']
193
+ lat: float
194
+ long: float
195
+ temperature: float
196
+ icon: Icon
197
+
198
+
199
+ class Bookmarks(BaseModel):
200
+ """Bookmarks model."""
201
+
202
+ cities: list[BookmarkCity]
203
+ nearest_city: Optional[BookmarkCity] = Field(None, alias='nearestCity')
204
+
205
+
206
+ class InObservation(BaseModel):
207
+ """Observation from public users."""
208
+
209
+ lat: float = Field(..., ge=-90.0, le=90.0)
210
+ long: float = Field(..., ge=-180.0, le=180.0)
211
+ description: str = Field(..., max_length=1024)
212
+ weather: int
213
+
214
+
215
+ class SensorLevel(BaseModel):
216
+ """Sensor level definition."""
217
+
218
+ level_type: Literal['height_above_ground'] = Field(..., alias='levelType')
219
+ unit: Literal['m']
220
+ value: float = Field(..., ge=0.0)
221
+
222
+
223
+ class ObservationMetadata(BaseModel):
224
+ """Sensor definition."""
225
+
226
+ id: str
227
+ name: str
228
+ description: str
229
+ data_type: Literal['realtime', 'climate'] = Field(..., alias='dataType')
230
+ unit: Literal['m', 'm/s', '%', '1/10 kt', 'degC', 'degrees', 'ft', 'hPa', 'mm']
231
+ category: Literal['Wind', 'Cloud Cover', 'Atmospheric pressure', 'Precipitation', 'Temperature', 'Humidity', 'Visibility']
232
+ performance_category: Literal['A', 'B', 'C', 'D', 'E'] = Field(..., alias='performanceCategory')
233
+ qualitycode: Literal[0, 1, 2, 3, 4, 5, 6, 7]
234
+ timeoffsets: Literal['PT0H']
235
+ timeresolution: Literal['PT1M', 'PT1H']
236
+ sensorlevels: Optional[SensorLevel] = None
237
+
238
+
239
+ class ObservationMetadataResponse(BaseModel):
240
+ """Elements metadata."""
241
+
242
+ licence: list[str] = ['Creative Commons', 'https://creativecommons.org/public-domain/cc0/']
243
+ doc_url: str = Field('/docs', alias='docUrl')
244
+ data: list[ObservationMetadata]
245
+ total_item_count: int = Field(1, alias='totalItemCount')
246
+ quality_codes: dict[str, str] = Field({'0': 'Value is controlled and found O.K.'}, alias='qualityCodes')
247
+ performance_category: dict[str, str] = Field(
248
+ {'A': 'The sensor type fulfills the requirements from WMO/CIMOs on measurement accuracy, calibration and maintenance.'}, alias='performanceCategory'
249
+ )
250
+
251
+
252
+ class ObservationResponseData(BaseModel):
253
+ """Model to link gendata id to their values."""
254
+
255
+ id: str
256
+ value: Union[int, float]
257
+
258
+
259
+ class ObservationResponse(BaseModel):
260
+ """Last Observations."""
261
+
262
+ licence: list[str] = ['Creative Commons', 'https://creativecommons.org/public-domain/cc0/']
263
+ doc_url: str = Field('/docs', alias='docUrl')
264
+ data: list[ObservationResponseData]
265
+ total_item_count: int = Field(1, alias='totalItemCount')
266
+ timestamp: datetime
267
+
268
+
269
+ class OutCity(BaseModel):
270
+ """City with translated name."""
271
+
272
+ id: int
273
+ name: str
274
+ region: Literal['N', 'S'] = 'S'
275
+ canton: Literal[
276
+ 'Capellen', 'Clervaux', 'Diekirch', 'Echternach', 'Esch-sur-Alzette', 'Grevenmacher', 'Luxembourg', 'Mersch', 'Redange', 'Remich', 'Vianden', 'Wiltz'
277
+ ]
278
+ domain: Literal['villes', 'lieu', 'fluvial']
279
+ lat: float
280
+ long: float
281
+
282
+
283
+ class VigilanceSettings(BaseModel):
284
+ """User settings for notifications."""
285
+
286
+ level: Literal[2, 3, 4]
287
+ type_air: bool = Field(False, alias='typeAir')
288
+ type_cold: bool = Field(False, alias='typeCold')
289
+ type_flooding: bool = Field(False, alias='typeFlooding')
290
+ type_heat: bool = Field(False, alias='typeHeat')
291
+ type_ice: bool = Field(False, alias='typeIce')
292
+ type_rain: bool = Field(False, alias='typeRain')
293
+ type_snow: bool = Field(False, alias='typeSnow')
294
+ type_storm: bool = Field(False, alias='typeStorm')
295
+ type_wind: Optional[bool] = Field(None, alias='typeWind')
296
+ zone_north: bool = Field(..., alias='zoneNorth')
297
+ zone_south: bool = Field(..., alias='zoneSouth')
298
+
299
+
300
+ class User(BaseModel):
301
+ """User model."""
302
+
303
+ language: Literal['fr', 'de', 'en', 'lb']
304
+ push_token: str = Field(..., alias='pushToken', max_length=50)
305
+ push_morning: bool = Field(False, alias='pushMorning')
306
+ push_evening: bool = Field(False, alias='pushEvening')
307
+ device: str
308
+ version: str
309
+ buildversion: str
310
+ vigilance: VigilanceSettings
311
+
312
+
313
+ class WeatherResponseForecast(BaseModel):
314
+ """Forecast model."""
315
+
316
+ current: CurrentWeather
317
+ hourly: list[HourlyWeather]
318
+ daily: list[DailyWeather]
319
+
320
+
321
+ class WeatherResponse(BaseModel):
322
+ """Final weather output from the backend."""
323
+
324
+ city: OutCity
325
+ forecast: WeatherResponseForecast
326
+ vigilances: list[Vigilance]
327
+ road_status: list[RoadStatusItem] = Field(..., alias='roadStatus')
328
+ ephemeris: Ephemeris
329
+ radar: Radar
330
+ satellite: Satellite
331
+ data: GraphicalData
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-meteolux
3
+ Version: 0.1.0
4
+ Summary: python library for accessing the MeteoLux REST API
5
+ Project-URL: Homepage, https://github.com/sim0nx/python-meteolux
6
+ Project-URL: Download, https://github.com/sim0nx/python-meteolux
7
+ Project-URL: Tracker, https://github.com/sim0nx/python-meteolux/issues
8
+ Project-URL: Documentation, http://python-meteolux.readthedocs.io/en/latest/?badge=latest
9
+ Project-URL: Source, https://github.com/sim0nx/python-meteolux
10
+ Author-email: Georges Toth <georges@trypill.org>
11
+ License: AGPLv3+
12
+ Keywords: MeteoLux
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: httpx
25
+ Requires-Dist: pydantic
26
+ Description-Content-Type: text/markdown
27
+
28
+ An early implementation of the MeteoLux REST API.
29
+
30
+ https://metapi.ana.lu/api/v1/docs
@@ -0,0 +1,7 @@
1
+ meteolux/__init__.py,sha256=SOvcvxmHp7DoSe68cohBEmGHrh7FyYLJ8or4HlCL03k,105
2
+ meteolux/async_api.py,sha256=0qs9bWBFOKbTSw2FAPiclWtEIKc7CPXV1QV1WTzJh40,8034
3
+ meteolux/exceptions.py,sha256=eGiv_oDBHfEDQ0vHNGnekELt43seRusYMk3vBqfSBgg,1630
4
+ meteolux/models.py,sha256=-J7mirv69yTm2RMl7tlH0qnLXs3qWTaTCKE1-kE893Q,8062
5
+ python_meteolux-0.1.0.dist-info/METADATA,sha256=alcXHlWcjNsL07sPX3v4xBlVaucVUqmeorw7VWOKdj0,1291
6
+ python_meteolux-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ python_meteolux-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any