diff --git a/README.md b/README.md index 7aea1a5..e0eb74c 100644 --- a/README.md +++ b/README.md @@ -34,21 +34,22 @@ pip install PyBMKG ```python import asyncio -from bmkg import BMKG -from bmkg.enums import Province +from bmkg import Earthquake, WeatherForecast async def main(): - async with BMKG() as bmkg: - weather_forecast = await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) - latest_earthquake = await bmkg.earthquake.get_latest_earthquake() - strong_earthquake = await bmkg.earthquake.get_strong_earthquake() - felt_earthquake = await bmkg.earthquake.get_felt_earthquake() + async with Earthquake() as earthquake: + latest_earthquake = await earthquake.get_latest_earthquake() + strong_earthquake = await earthquake.get_strong_earthquake() + felt_earthquake = await earthquake.get_felt_earthquake() - print(f"Weather Forecast: {weather_forecast}") print(f"Latest Earthquakes: {latest_earthquake}") print(f"Strong Earthquakes: {strong_earthquake}") print(f"Felt Earthquakes: {felt_earthquake}") + async with WeatherForecast() as weather_forecast: + weather_forecast = await weather_forecast.get_weather_forecast("11.01.01.2001") + print(f"Weather Forecast: {weather_forecast}") + asyncio.run(main()) ``` diff --git a/docs/index.md b/docs/index.md index 020b4fb..359532e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,21 +34,22 @@ pip install PyBMKG ```python import asyncio -from bmkg import BMKG -from bmkg.enums import Province +from bmkg import Earthquake, WeatherForecast async def main(): - async with BMKG() as bmkg: - weather_forecast = await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) - latest_earthquake = await bmkg.earthquake.get_latest_earthquake() - strong_earthquake = await bmkg.earthquake.get_strong_earthquake() - felt_earthquake = await bmkg.earthquake.get_felt_earthquake() + async with Earthquake() as earthquake: + latest_earthquake = await earthquake.get_latest_earthquake() + strong_earthquake = await earthquake.get_strong_earthquake() + felt_earthquake = await earthquake.get_felt_earthquake() - print(f"Weather Forecast: {weather_forecast}") print(f"Latest Earthquakes: {latest_earthquake}") print(f"Strong Earthquakes: {strong_earthquake}") print(f"Felt Earthquakes: {felt_earthquake}") + async with WeatherForecast() as weather_forecast: + weather_forecast = await weather_forecast.get_weather_forecast("11.01.01.2001") + print(f"Weather Forecast: {weather_forecast}") + asyncio.run(main()) ``` diff --git a/docs/tutorials.md b/docs/tutorials.md index 4944561..9ef2e57 100644 --- a/docs/tutorials.md +++ b/docs/tutorials.md @@ -27,67 +27,34 @@ for more details. !!! example - === "Facade" - - ```python - import asyncio - from dataclasses import fields - - from bmkg import BMKG - - - async def main(): - async with BMKG() as bmkg: - latest_earthquake = await bmkg.earthquake.get_latest_earthquake() - - print(latest_earthquake.earthquake.datetime) - print(latest_earthquake.earthquake.coordinate) - print( - latest_earthquake.earthquake.magnitude, - fields(latest_earthquake.earthquake)[2].metadata["unit"], - ) - print( - latest_earthquake.earthquake.depth, - fields(latest_earthquake.earthquake)[3].metadata["unit"], - ) - print(latest_earthquake.earthquake.region) - print(latest_earthquake.potency) - print(latest_earthquake.felt) - print(latest_earthquake.shakemap) - - asyncio.run(main()) - ``` - - === "Non Facade" - - ```python - import asyncio - from dataclasses import fields - - from bmkg.api import Earthquake - - - async def main(): - async with Earthquake() as earthquake: - latest_earthquake = await earthquake.get_latest_earthquake() - - print(latest_earthquake.earthquake.datetime) - print(latest_earthquake.earthquake.coordinate) - print( - latest_earthquake.earthquake.magnitude, - fields(latest_earthquake.earthquake)[2].metadata["unit"], - ) - print( - latest_earthquake.earthquake.depth, - fields(latest_earthquake.earthquake)[3].metadata["unit"], - ) - print(latest_earthquake.earthquake.region) - print(latest_earthquake.potency) - print(latest_earthquake.felt) - print(latest_earthquake.shakemap) - - asyncio.run(main()) - ``` + ```python + import asyncio + from dataclasses import fields + + from bmkg import Earthquake + + + async def main(): + async with Earthquake() as earthquake: + latest_earthquake = await earthquake.get_latest_earthquake() + + print(latest_earthquake.earthquake.datetime) + print(latest_earthquake.earthquake.coordinate) + print( + latest_earthquake.earthquake.magnitude, + fields(latest_earthquake.earthquake)[2].metadata["unit"], + ) + print( + latest_earthquake.earthquake.depth, + fields(latest_earthquake.earthquake)[3].metadata["unit"], + ) + print(latest_earthquake.earthquake.region) + print(latest_earthquake.potency) + print(latest_earthquake.felt) + print(latest_earthquake.shakemap) + + asyncio.run(main()) + ``` Output: @@ -112,65 +79,33 @@ for more details. !!! example - === "Facade" - - ```python - import asyncio - from dataclasses import fields - - from bmkg import BMKG - - - async def main(): - async with BMKG() as bmkg: - strong_earthquakes = await bmkg.earthquake.get_strong_earthquake() - - for strong_earthquake in strong_earthquakes: - print(strong_earthquake.earthquake.datetime) - print(strong_earthquake.earthquake.coordinate) - print( - strong_earthquake.earthquake.magnitude, - fields(strong_earthquake.earthquake)[2].metadata["unit"], - ) - print( - strong_earthquake.earthquake.depth, - fields(strong_earthquake.earthquake)[3].metadata["unit"], - ) - print(strong_earthquake.earthquake.region) - print(strong_earthquake.potency) + ```python + import asyncio + from dataclasses import fields - asyncio.run(main()) - ``` + from bmkg import Earthquake - === "Non Facade" - ```python - import asyncio - from dataclasses import fields + async def main(): + async with Earthquake() as earthquake: + strong_earthquakes = await earthquake.get_strong_earthquake() - from bmkg.api import Earthquake - - - async def main(): - async with Earthquake() as earthquake: - strong_earthquakes = await earthquake.get_strong_earthquake() - - for strong_earthquake in strong_earthquakes: - print(strong_earthquake.earthquake.datetime) - print(strong_earthquake.earthquake.coordinate) - print( - strong_earthquake.earthquake.magnitude, - fields(strong_earthquake.earthquake)[2].metadata["unit"], - ) - print( - strong_earthquake.earthquake.depth, - fields(strong_earthquake.earthquake)[3].metadata["unit"], - ) - print(strong_earthquake.earthquake.region) - print(strong_earthquake.potency) + for strong_earthquake in strong_earthquakes: + print(strong_earthquake.earthquake.datetime) + print(strong_earthquake.earthquake.coordinate) + print( + strong_earthquake.earthquake.magnitude, + fields(strong_earthquake.earthquake)[2].metadata["unit"], + ) + print( + strong_earthquake.earthquake.depth, + fields(strong_earthquake.earthquake)[3].metadata["unit"], + ) + print(strong_earthquake.earthquake.region) + print(strong_earthquake.potency) - asyncio.run(main()) - ``` + asyncio.run(main()) + ``` Output: @@ -193,65 +128,33 @@ for more details. !!! example - === "Facade" + ```python + import asyncio + from dataclasses import fields - ```python - import asyncio - from dataclasses import fields + from bmkg import Earthquake - from bmkg import BMKG + async def main(): + async with Earthquake() as earthquake: + felt_earthquakes = await earthquake.get_felt_earthquake() - async def main(): - async with BMKG() as bmkg: - felt_earthquakes = await bmkg.earthquake.get_felt_earthquake() - - for felt_earthquake in felt_earthquakes: - print(felt_earthquake.earthquake.datetime) - print(felt_earthquake.earthquake.coordinate) - print( - felt_earthquake.earthquake.magnitude, - fields(felt_earthquake.earthquake)[2].metadata["unit"], - ) - print( - felt_earthquake.earthquake.depth, - fields(felt_earthquake.earthquake)[3].metadata["unit"], - ) - print(felt_earthquake.earthquake.region) - print(felt_earthquake.felt) - - asyncio.run(main()) - ``` - - === "Non Facade" - - ```python - import asyncio - from dataclasses import fields - - from bmkg.api import Earthquake - - - async def main(): - async with Earthquake() as earthquake: - felt_earthquakes = await earthquake.get_felt_earthquake() - - for felt_earthquake in felt_earthquakes: - print(felt_earthquake.earthquake.datetime) - print(felt_earthquake.earthquake.coordinate) - print( - felt_earthquake.earthquake.magnitude, - fields(felt_earthquake.earthquake)[2].metadata["unit"], - ) - print( - felt_earthquake.earthquake.depth, - fields(felt_earthquake.earthquake)[3].metadata["unit"], - ) - print(felt_earthquake.earthquake.region) - print(felt_earthquake.felt) + for felt_earthquake in felt_earthquakes: + print(felt_earthquake.earthquake.datetime) + print(felt_earthquake.earthquake.coordinate) + print( + felt_earthquake.earthquake.magnitude, + fields(felt_earthquake.earthquake)[2].metadata["unit"], + ) + print( + felt_earthquake.earthquake.depth, + fields(felt_earthquake.earthquake)[3].metadata["unit"], + ) + print(felt_earthquake.earthquake.region) + print(felt_earthquake.felt) - asyncio.run(main()) - ``` + asyncio.run(main()) + ``` Output: @@ -280,43 +183,21 @@ for more details. !!! example - === "Facade" - - ```python - import asyncio - - from bmkg import BMKG - - - async def main(): - async with BMKG() as bmkg: - latest_earthquake = await bmkg.earthquake.get_latest_earthquake() - shakemap = latest_earthquake.shakemap - shakemap_content = await shakemap.get_content() - - print(shakemap.file_name) - print(shakemap_content) + ```python + import asyncio - asyncio.run(main()) - ``` + from bmkg import Shakemap - === "Non Facade" - ```python - import asyncio + async def main(): + async with Shakemap("20240203152510.mmi.jpg") as shakemap: + shakemap_content = await shakemap.get_content() - from bmkg.api import Shakemap + print(shakemap.file_name) + print(shakemap_content) - - async def main(): - async with Shakemap("20240203152510.mmi.jpg") as shakemap: - shakemap_content = await shakemap.get_content() - - print(shakemap.file_name) - print(shakemap_content) - - asyncio.run(main()) - ``` + asyncio.run(main()) + ``` Output: @@ -332,107 +213,41 @@ namely get_weather_forecast. ### get_weather_forecast -get_weather_forecast is used to get weather forecast -information for all districts and cities in Indonesia -within three days. There are 35 weather forecast data -representing provinces and major cities in Indonesia. -For each area you will get twelve weather forecasts data -so there are four weather forecasts for one day. The link -starts with `https://data.bmkg.go.id/DataMKG/MEWS/DigitalForecast` -and followed with `/DigitalForecast-{PROVINCE_NAME}.xml`. Read +get_weather_forecast provides weather forecast information for all districts and cities across Indonesia, covering a three-day period. The data is organized by region, identified using a unique "Region IV" code, which follows the format W.X.Y.Z (e.g., "11.01.01.2001"). A comprehensive list of available region codes can be found at [kodewilayah.id](https://kodewilayah.id). + +The forecast includes detailed weather information for each region, with updates every three hours for the next three days. The weather data is refreshed every two days. To access the forecast, use the API endpoint `https://api.bmkg.go.id/publik/prakiraan-cuaca?adm4={region_code}`. Read [get_weather_forecast reference](reference/api.md/#bmkg.api.WeatherForecast.get_weather_forecast) for more details. !!! example - === "Facade" - - ```python - import asyncio - - from bmkg import BMKG - from bmkg.enums import Province, Type - - - async def main(): - async with BMKG() as bmkg: - weather_forecast_data = await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) + ```python + import asyncio - print(weather_forecast_data.data) - print(weather_forecast_data.forecast) - print(weather_forecast_data.issue) + from bmkg import WeatherForecast - for area, weathers in weather_forecast_data.weathers.items(): - if area.type == Type.LAND: - print(area) - for weather in weathers: - print(weather.datetime) - print(weather.weather) - print(weather.temperature) - print(weather.minimum_temperature) - print(weather.maximum_temperature) - print(weather.humidity) - print(weather.min_humidity) - print(weather.max_humidity) - print(weather.wind_direction) - print(weather.wind_speed) + async def main(): + async with WeatherForecast() as weather_forecast: + weather_forecast = await weather_forecast.get_weather_forecast("11.01.01.2001") - asyncio.run(main()) - ``` + print(weather_forecast.location) - === "Non Facade" + for weather in weather_forecast.weathers: + print(weather) - ```python - import asyncio - - from bmkg.api import WeatherForecast - from bmkg.enums import Province, Type - - - async def main(): - async with WeatherForecast() as weather_forecast: - weather_forecast_data = await weather_forecast.get_weather_forecast(Province.ACEH) - - print(weather_forecast_data.data) - print(weather_forecast_data.forecast) - print(weather_forecast_data.issue) - - for area, weathers in weather_forecast_data.weathers.items(): - if area.type == Type.LAND: - print(area) - - for weather in weathers: - print(weather.datetime) - print(weather.weather) - print(weather.temperature) - print(weather.minimum_temperature) - print(weather.maximum_temperature) - print(weather.humidity) - print(weather.min_humidity) - print(weather.max_humidity) - print(weather.wind_direction) - print(weather.wind_speed) - - asyncio.run(main()) - ``` + asyncio.run(main()) + ``` Output: ```console - Data(source='meteofactory', productioncenter='NC Jakarta') - Forecast(domain='local') - 2024-01-18 03:13:02 - Area(id='501409', coordinate=Coordinate(latitude=4.176594, longitude=96.124878), type=, region='', level='1', description='Aceh Barat', domain='Aceh', tags='', names=Name(en_US='Aceh Barat', id_ID='Kab. Aceh Barat')) - 2024-01-18 00:00:00 - 1 - Temperature(celcius=25.0, fahrenheit=77.0) - Temperature(celcius=25.0, fahrenheit=77.0) - Temperature(celcius=32.0, fahrenheit=89.6) - Humidity(percentage=95) - Humidity(percentage=60) - Humidity(percentage=95) - WindDirection(deg=67.5, card=, sexa=) - WindSpeed(knot=5.0, mph=5.75389725, kph=9.26, ms=2.57222222) + Location(admin_level_1='11', admin_level_2='11.01', admin_level_3='11.01.01', admin_level_4='11.01.01.2001', province='Aceh', city='Aceh Selatan', subdistrict='Bakongan', village='Keude Bakongan', longitude=97.4845840426, latitude=2.9310948032, timezone='Asia/Jakarta') + Weather(datetime=datetime.datetime(2024, 12, 29, 0, 0, tzinfo=datetime.timezone.utc), t=24, tcc=100, tp=0.0, weather=, wd_deg=72, wd=, wd_to=, ws=4.0, hu=95, vs=18158, time_index='-3-0', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 0, 0), local_datetime=datetime.datetime(2024, 12, 29, 7, 0)) + Weather(datetime=datetime.datetime(2024, 12, 29, 3, 0, tzinfo=datetime.timezone.utc), t=28, tcc=100, tp=0.0, weather=, wd_deg=171, wd=, wd_to=, ws=2.9, hu=78, vs=45315, time_index='0-3', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 3, 0), local_datetime=datetime.datetime(2024, 12, 29, 10, 0)) + Weather(datetime=datetime.datetime(2024, 12, 29, 6, 0, tzinfo=datetime.timezone.utc), t=28, tcc=99, tp=0.8, weather=, wd_deg=205, wd=, wd_to=, ws=8.5, hu=77, vs=23196, time_index='3-6', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 6, 0), local_datetime=datetime.datetime(2024, 12, 29, 13, 0)) + Weather(datetime=datetime.datetime(2024, 12, 29, 9, 0, tzinfo=datetime.timezone.utc), t=28, tcc=100, tp=1.3, weather=, wd_deg=89, wd=, wd_to=, ws=1.9, hu=81, vs=29860, time_index='6-9', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 9, 0), local_datetime=datetime.datetime(2024, 12, 29, 16, 0)) + Weather(datetime=datetime.datetime(2024, 12, 29, 12, 0, tzinfo=datetime.timezone.utc), t=26, tcc=100, tp=1.2, weather=, wd_deg=141, wd=, wd_to=, ws=7.7, hu=90, vs=14741, time_index='9-12', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 12, 0), local_datetime=datetime.datetime(2024, 12, 29, 19, 0)) + Weather(datetime=datetime.datetime(2024, 12, 29, 15, 0, tzinfo=datetime.timezone.utc), t=25, tcc=99, tp=2.1, weather=, wd_deg=112, wd=, wd_to=, ws=6.1, hu=90, vs=15254, time_index='12-15', analysis_date=datetime.datetime(2024, 12, 29, 0, 0), image='https://api-apps.bmkg.go.id/storage/icon/cuaca/berawan-am.svg', utc_datetime=datetime.datetime(2024, 12, 29, 15, 0), local_datetime=datetime.datetime(2024, 12, 29, 22, 0)) ... ``` diff --git a/poetry.lock b/poetry.lock index 6701d86..bc8e194 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -398,17 +398,6 @@ files = [ [package.extras] toml = ["tomli"] -[[package]] -name = "defusedxml" -version = "0.7.1" -description = "XML bomb protection for Python stdlib modules" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, - {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, -] - [[package]] name = "frozenlist" version = "1.4.1" @@ -1578,17 +1567,6 @@ files = [ [package.dependencies] pbr = ">=2.0.0" -[[package]] -name = "types-defusedxml" -version = "0.7.0.20240218" -description = "Typing stubs for defusedxml" -optional = false -python-versions = ">=3.8" -files = [ - {file = "types-defusedxml-0.7.0.20240218.tar.gz", hash = "sha256:05688a7724dc66ea74c4af5ca0efc554a150c329cb28c13a64902cab878d06ed"}, - {file = "types_defusedxml-0.7.0.20240218-py3-none-any.whl", hash = "sha256:2b7f3c5ca14fdbe728fab0b846f5f7eb98c4bd4fd2b83d25f79e923caa790ced"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1758,4 +1736,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "e37ef022435343820a6773f23a087730dcd4b11e4a7d9ce71c6b0a1c7d395f63" +content-hash = "9cf3f831720e7fd11e6587af6decd649fcfc3362db9abee3cfc7bbd8d82cb681" diff --git a/pyproject.toml b/pyproject.toml index 6e502e1..e47846f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "PyBMKG" -version = "2.1.1" +version = "3.0.0" description = "Python BMKG API Wrapper" authors = ["Kira "] maintainers = ["Kira ", "vexra "] @@ -27,9 +27,7 @@ classifiers = [ [tool.poetry.dependencies] aiohttp = "^3.9.1" -defusedxml = "^0.7.1" python = "^3.11" -types-defusedxml = "^0.7.0.20240218" [tool.poetry.group.dev.dependencies] bandit = "^1.7.6" diff --git a/src/bmkg/__init__.py b/src/bmkg/__init__.py index 6724f8b..c4355e7 100644 --- a/src/bmkg/__init__.py +++ b/src/bmkg/__init__.py @@ -1,3 +1,7 @@ -from .api import BMKG +from .api import Earthquake, Shakemap, WeatherForecast -__all__ = ["BMKG"] +__all__ = [ + "Earthquake", + "Shakemap", + "WeatherForecast", +] diff --git a/src/bmkg/api/__init__.py b/src/bmkg/api/__init__.py index ecd3e2d..bf186ee 100644 --- a/src/bmkg/api/__init__.py +++ b/src/bmkg/api/__init__.py @@ -1,10 +1,8 @@ -from .bmkg import BMKG from .earthquake import Earthquake from .shakemap import Shakemap from .weather_forecast import WeatherForecast __all__ = [ - "BMKG", "Earthquake", "Shakemap", "WeatherForecast", diff --git a/src/bmkg/api/api.py b/src/bmkg/api/api.py index e126065..9a3be02 100644 --- a/src/bmkg/api/api.py +++ b/src/bmkg/api/api.py @@ -13,9 +13,12 @@ class API: """ def __init__(self, session: ClientSession | None = None) -> None: - self._session = ( - session if session is not None else ClientSession("https://data.bmkg.go.id") - ) + if not hasattr(self, "base_url"): + raise NotImplementedError( + f"{self.__class__.__name__} must define 'base_url'." + ) + + self._session = session if session is not None else ClientSession(self.base_url) async def __aenter__(self) -> Self: return self diff --git a/src/bmkg/api/bmkg.py b/src/bmkg/api/bmkg.py deleted file mode 100644 index 392d959..0000000 --- a/src/bmkg/api/bmkg.py +++ /dev/null @@ -1,23 +0,0 @@ -from aiohttp import ClientSession - -from .api import API -from .earthquake import Earthquake -from .weather_forecast import WeatherForecast - -__all__ = ["BMKG"] - - -class BMKG(API): - """ - Base BMKG API wrapper. - - Attributes: - earthquake (Earthquake): earthquake api interface. - weather_forecast (WeatherForecast): weather forecast api interface. - """ - - def __init__(self, session: ClientSession | None = None) -> None: - API.__init__(self, session) - - self.earthquake = Earthquake(self._session) - self.weather_forecast = WeatherForecast(self._session) diff --git a/src/bmkg/api/earthquake.py b/src/bmkg/api/earthquake.py index 8a02776..508674f 100644 --- a/src/bmkg/api/earthquake.py +++ b/src/bmkg/api/earthquake.py @@ -17,6 +17,7 @@ class Earthquake(API): Earthquake API Wrapper from BMKG API. """ + base_url = "https://data.bmkg.go.id" url = "/DataMKG/TEWS" async def get_latest_earthquake(self) -> schemas.LatestEarthquake: @@ -28,17 +29,17 @@ async def get_latest_earthquake(self) -> schemas.LatestEarthquake: Examples: >>> import asyncio - >>> from bmkg import BMKG + >>> from bmkg import Earthquake >>> async def main(): - ... async with BMKG() as bmkg: - ... latest_earthquake = await bmkg.earthquake.get_latest_earthquake() + ... async with Earthquake() as earthquake: + ... latest_earthquake = await earthquake.get_latest_earthquake() ... print(latest_earthquake) >>> asyncio.run(main()) LatestEarthquake(earthquake=Earthquake(datetime=datetime.datetime(...) Notes: The `LatestEarthquake` schema has a `shakemap` field which is the `Shakemap` API. - """ # noqa: E501 + """ async with self._session.get(f"{self.url}/autogempa.json") as response: latest_earthquake = parse_latest_earthquake_data(await response.json()) # type: ignore @@ -57,14 +58,14 @@ async def get_strong_earthquake(self) -> Iterator[schemas.StrongEarthquake]: Examples: >>> import asyncio - >>> from bmkg import BMKG + >>> from bmkg import Earthquake >>> async def main(): - ... async with BMKG() as bmkg: - ... strong_earthquake = await bmkg.earthquake.get_strong_earthquake() + ... async with Earthquake() as earthquake: + ... strong_earthquake = await earthquake.get_strong_earthquake() ... print(strong_earthquake) >>> asyncio.run(main()) - """ # noqa: E501 + """ async with self._session.get(f"{self.url}/gempaterkini.json") as response: return parse_strong_earthquake_data(await response.json()) # type: ignore @@ -77,10 +78,10 @@ async def get_felt_earthquake(self) -> Iterator[schemas.FeltEarthquake]: Examples: >>> import asyncio - >>> from bmkg import BMKG + >>> from bmkg import Earthquake >>> async def main(): - ... async with BMKG() as bmkg: - ... felt_earthquake = await bmkg.earthquake.get_felt_earthquake() + ... async with Earthquake() as earthquake: + ... felt_earthquake = await earthquake.get_felt_earthquake() ... print(felt_earthquake) >>> asyncio.run(main()) diff --git a/src/bmkg/api/shakemap.py b/src/bmkg/api/shakemap.py index 177d25c..5738c0e 100644 --- a/src/bmkg/api/shakemap.py +++ b/src/bmkg/api/shakemap.py @@ -11,6 +11,7 @@ class Shakemap(API, schemas.Shakemap): Shakemap API Wrapper from BMKG API. """ + base_url = "https://data.bmkg.go.id" url = "/DataMKG/TEWS" def __init__(self, file_name: str, session: ClientSession | None = None) -> None: @@ -26,15 +27,15 @@ async def get_content(self) -> bytes: Examples: >>> import asyncio - >>> from bmkg import BMKG + >>> from bmkg import Earthquake >>> async def main(): - ... async with BMKG() as bmkg: - ... latest_earthquake = await bmkg.earthquake.get_latest_earthquake() + ... async with Earthquake() as earthquake: + ... latest_earthquake = await earthquake.get_latest_earthquake() ... shakemap = latest_earthquake.shakemap ... shakemap_content = await shakemap.get_content() ... print(shakemap_content) >>> asyncio.run(main()) b'...' - """ # noqa: E501 + """ async with self._session.get(f"{self.url}/{self.file_name}") as response: return await response.read() diff --git a/src/bmkg/api/weather_forecast.py b/src/bmkg/api/weather_forecast.py index 3a26ace..976b508 100644 --- a/src/bmkg/api/weather_forecast.py +++ b/src/bmkg/api/weather_forecast.py @@ -1,4 +1,3 @@ -from ..enums import Province from ..parsers import parse_weather_forecast_data from ..schemas import WeatherForecast as WeatherForecastData from .api import API @@ -11,31 +10,35 @@ class WeatherForecast(API): Weather Forecast API Wrapper from BMKG API. """ - url = "/DataMKG/MEWS/DigitalForecast" + base_url = "https://api.bmkg.go.id" + url = "/publik/prakiraan-cuaca" - async def get_weather_forecast(self, province: Province) -> WeatherForecastData: + async def get_weather_forecast(self, region_code: str) -> WeatherForecastData: """ Request weather forecast from weather forecast API. Args: - province: A `Province` enum symbolic names (members). + region_code: The administrative region code (level IV) for a + subdistrict or village in Indonesia. The code is formatted as `W.X.Y.Z` + (e.g., `"11.01.01.2001"`). You can find the list of available region codes + at https://kodewilayah.id. Returns: A `WeatherForecastData` schema. Examples: >>> import asyncio - >>> from bmkg import BMKG + >>> from bmkg import WeatherForecast >>> async def main(): - ... async with BMKG() as bmkg: - ... weather_forecast_data = ( - ... await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) + ... async with WeatherForecast() as weather_forecast: + ... weather_forecast_data = await weather_forecast.get_weather_forecast( + ... "11.01.01.2001" ... ) ... print(weather_forecast_data) >>> asyncio.run(main()) - WeatherForecast(data=Data(source=...) - """ # noqa: E501 + WeatherForecast(location=Location(admin_level_1=...) + """ async with self._session.get( - f"{self.url}/DigitalForecast-{province}.xml" + self.url, params={"adm4": region_code} ) as response: - return parse_weather_forecast_data(await response.read()) + return parse_weather_forecast_data(await response.json()) diff --git a/src/bmkg/enums/__init__.py b/src/bmkg/enums/__init__.py index db6dd12..5344414 100644 --- a/src/bmkg/enums/__init__.py +++ b/src/bmkg/enums/__init__.py @@ -3,15 +3,9 @@ """ from .cardinal import Cardinal -from .province import Province -from .sexa import Sexa -from .type import Type from .weather import Weather __all__ = [ "Cardinal", - "Province", - "Sexa", - "Type", "Weather", ] diff --git a/src/bmkg/enums/province.py b/src/bmkg/enums/province.py deleted file mode 100644 index b2ae8df..0000000 --- a/src/bmkg/enums/province.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -Module to store a str enum class representation the name of the province. -""" - -from enum import StrEnum - -__all__ = ["Province"] - - -class Province(StrEnum): - """ - A str enum class that define valid province. - - Attributes: - ACEH: `"Aceh"` - BALI: `"Bali"` - BANGKA_BELITUNG: `"BangkaBelitung"` - BANTEN: `"Banten"` - BENGKULU: `"Bengkulu"` - DI_YOGYAKARTA: `"DIYogyakarta"` - DKI_JAKARTA: `"DKIJakarta"` - GORONTALO: `"Gorontalo"` - JAMBI: `"Jambi"` - JAWA_BARAT: `"JawaBarat"` - JAWA_TENGAH: `"JawaTengah"` - JAWA_TIMUR: `"JawaTimur"` - KALIMANTAN_BARAT: `"KalimantanBarat"` - KALIMANTAN_SELATAN: `"KalimantanSelatan"` - KALIMANTAN_TENGAH: `"KalimantanTengah"` - KALIMANTAN_TIMUR: `"KalimantanTimur"` - KALIMANTAN_UTARA: `"KalimantanUtara"` - KEPULAUAN_RIAU: `"KepulauanRiau"` - LAMPUNG: `"Lampung"` - MALUKU: `"Maluku"` - MALUKU_UTARA: `"MalukuUtara"` - NUSA_TENGGARA_BARAT: `"NusaTenggaraBarat"` - NUSA_TENGGARA_TIMUR: `"NusaTenggaraTimur"` - PAPUA: `"Papua"` - PAPUA_BARAT: `"PapuaBarat"` - RIAU: `"Riau"` - SULAWESI_BARAT: `"SulawesiBarat"` - SULAWESI_SELATAN: `"SulawesiSelatan"` - SULAWESI_TENGAH: `"SulawesiTengah"` - SULAWESI_TENGGARA: `"SulawesiTenggara"` - SULAWESI_UTARA: `"SulawesiUtara"` - SUMATERA_BARAT: `"SumateraBarat"` - SUMATERA_SELATAN: `"SumateraSelatan"` - SUMATERA_UTARA: `"SumateraUtara"` - INDONESIA: `"Indonesia"` - - Examples: - >>> Province("Aceh") - - >>> Province["ACEH"] - - >>> Province.ACEH - - >>> Province.ACEH == "Aceh" - True - >>> print(Province.ACEH) - Aceh - - Note: - `INDONESIA` is the country name. - """ - - ACEH: str = "Aceh" - BALI: str = "Bali" - BANGKA_BELITUNG: str = "BangkaBelitung" - BANTEN: str = "Banten" - BENGKULU: str = "Bengkulu" - DI_YOGYAKARTA: str = "DIYogyakarta" - DKI_JAKARTA: str = "DKIJakarta" - GORONTALO: str = "Gorontalo" - JAMBI: str = "Jambi" - JAWA_BARAT: str = "JawaBarat" - JAWA_TENGAH: str = "JawaTengah" - JAWA_TIMUR: str = "JawaTimur" - KALIMANTAN_BARAT: str = "KalimantanBarat" - KALIMANTAN_SELATAN: str = "KalimantanSelatan" - KALIMANTAN_TENGAH: str = "KalimantanTengah" - KALIMANTAN_TIMUR: str = "KalimantanTimur" - KALIMANTAN_UTARA: str = "KalimantanUtara" - KEPULAUAN_RIAU: str = "KepulauanRiau" - LAMPUNG: str = "Lampung" - MALUKU: str = "Maluku" - MALUKU_UTARA: str = "MalukuUtara" - NUSA_TENGGARA_BARAT: str = "NusaTenggaraBarat" - NUSA_TENGGARA_TIMUR: str = "NusaTenggaraTimur" - PAPUA: str = "Papua" - PAPUA_BARAT: str = "PapuaBarat" - RIAU: str = "Riau" - SULAWESI_BARAT: str = "SulawesiBarat" - SULAWESI_SELATAN: str = "SulawesiSelatan" - SULAWESI_TENGAH: str = "SulawesiTengah" - SULAWESI_TENGGARA: str = "SulawesiTenggara" - SULAWESI_UTARA: str = "SulawesiUtara" - SUMATERA_BARAT: str = "SumateraBarat" - SUMATERA_SELATAN: str = "SumateraSelatan" - SUMATERA_UTARA: str = "SumateraUtara" - INDONESIA: str = "Indonesia" diff --git a/src/bmkg/enums/sexa.py b/src/bmkg/enums/sexa.py deleted file mode 100644 index 68ab86c..0000000 --- a/src/bmkg/enums/sexa.py +++ /dev/null @@ -1,71 +0,0 @@ -""" -Module to store a str enum class representation sexa directions. -""" - -from enum import StrEnum - -__all__ = ["Sexa"] - - -class Sexa(StrEnum): - """ - A str enum class that define valid sexa direction. - - Attributes: - NORTH_NORTHEAST: `"2230"` - NORTHEAST: `"4500"` - EAST_NORTHEAST: `"6730"` - EAST: `"9000"` - EAST_SOUTHEAST: `"11230"` - SOUTHEAST: `"13500"` - SOUTH_SOUTHEAST: `"15730"` - SOUTH: `"18000"` - SOUTH_SOUTHWEST: `"20230"` - SOUTHWEST: `"22500"` - WEST_SOUTHWEST: `"24730"` - WEST: `"27000"` - WEST_NORTHWEST: `"29230"` - NORTHWEST: `"31500"` - NORTH_NORTHWEST: `"33730"` - VARIABLE: `"000"` - - Examples: - >>> Sexa("2230") - - >>> Sexa["NORTH_NORTHEAST"] - - >>> Sexa.NORTH_NORTHEAST - - >>> Sexa.NORTH_NORTHEAST == "2230" - True - >>> print(Sexa.NORTH_NORTHEAST) - 2230 - - Note: - `VARIABLE` direction mean as it's name suggest, the direction can't be - determined. - - Warning: - There is no `NORTH` due to bug in BMKG API. - """ - - # FIXME - # Change NORTH or VARIABLE if the bug was fixed - # Possible bug in BMKG API that NORTH equal to VARIABLE - # NORTH: str = "000" - NORTH_NORTHEAST: str = "2230" - NORTHEAST: str = "4500" - EAST_NORTHEAST: str = "6730" - EAST: str = "9000" - EAST_SOUTHEAST: str = "11230" - SOUTHEAST: str = "13500" - SOUTH_SOUTHEAST: str = "15730" - SOUTH: str = "18000" - SOUTH_SOUTHWEST: str = "20230" - SOUTHWEST: str = "22500" - WEST_SOUTHWEST: str = "24730" - WEST: str = "27000" - WEST_NORTHWEST: str = "29230" - NORTHWEST: str = "31500" - NORTH_NORTHWEST: str = "33730" - VARIABLE: str = "000" diff --git a/src/bmkg/enums/type.py b/src/bmkg/enums/type.py deleted file mode 100644 index 8c97662..0000000 --- a/src/bmkg/enums/type.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Module to store a str enum class representation area type. -""" - -from enum import StrEnum - -__all__ = ["Type"] - - -class Type(StrEnum): - """ - A str enum class that define valid area type. - - Attributes: - LAND: `"land"` - SEA: `"sea"` - - Examples: - >>> Type("land") - - >>> Type["LAND"] - - >>> Type.LAND - - >>> Type.LAND == "land" - True - >>> print(Type.LAND) - land - """ - - LAND: str = "land" - SEA: str = "sea" diff --git a/src/bmkg/exceptions.py b/src/bmkg/exceptions.py index 400dbf7..da7d8cb 100644 --- a/src/bmkg/exceptions.py +++ b/src/bmkg/exceptions.py @@ -1,6 +1,5 @@ __all__ = [ "BMKGError", - "WeatherForecastParseError", ] @@ -10,11 +9,3 @@ class BMKGError(Exception): """ pass - - -class WeatherForecastParseError(BMKGError): - """ - Weather forecast parse exception. - """ - - pass diff --git a/src/bmkg/parsers/__init__.py b/src/bmkg/parsers/__init__.py index 597ff36..ed83f63 100644 --- a/src/bmkg/parsers/__init__.py +++ b/src/bmkg/parsers/__init__.py @@ -1,33 +1,17 @@ -from .parse_area_element import parse_area_element -from .parse_data_element import parse_data_element -from .parse_datetime_element import parse_datetime_element from .parse_earthquake_data import parse_earthquake_data from .parse_felt_earthquake_data import parse_felt_earthquake_data -from .parse_forecast_element import parse_forecast_element -from .parse_humidity_element import parse_humidity_element -from .parse_issue_element import parse_issue_element from .parse_latest_earthquake_data import parse_latest_earthquake_data +from .parse_location_data import parse_location_data from .parse_strong_earthquake_data import parse_strong_earthquake_data -from .parse_temperature_element import parse_temperature_element -from .parse_weather_element import parse_weather_element +from .parse_weather_data import parse_weather_data from .parse_weather_forecast_data import parse_weather_forecast_data -from .parse_wind_direction_element import parse_wind_direction_element -from .parse_wind_speed_element import parse_wind_speed_element __all__ = [ "parse_earthquake_data", "parse_felt_earthquake_data", "parse_latest_earthquake_data", + "parse_location_data", "parse_strong_earthquake_data", - "parse_data_element", - "parse_forecast_element", - "parse_issue_element", - "parse_area_element", - "parse_humidity_element", - "parse_temperature_element", - "parse_weather_element", - "parse_wind_direction_element", - "parse_wind_speed_element", - "parse_datetime_element", + "parse_weather_data", "parse_weather_forecast_data", ] diff --git a/src/bmkg/parsers/parse_area_element.py b/src/bmkg/parsers/parse_area_element.py deleted file mode 100644 index e1f2654..0000000 --- a/src/bmkg/parsers/parse_area_element.py +++ /dev/null @@ -1,112 +0,0 @@ -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..enums import Type -from ..exceptions import WeatherForecastParseError -from ..schemas import ( - Area, - Coordinate, - Name, -) - -__all__ = ["parse_area_element"] - - -def parse_area_element(element: Element) -> Area: - """ - Parse area element tree in xml. - - Args: - element: an element of area - - Returns: - An `Area` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... "" - ... ' Aceh Barat' - ... ' Kab. Aceh Barat' - ... "" - ... ) - >>> area = parse_area_element(element) - >>> area - Area(id='501409', coordinate=Coordinate(latitude=4.176594, longitude=96.124878), ... - """ - id = element.get("id") - if id is None: - raise WeatherForecastParseError("id attribute in area tag not found") - - latitude = element.get("latitude") - if latitude is None: - raise WeatherForecastParseError("latitude attribute in area tag not found") - - longitude = element.get("longitude") - if longitude is None: - raise WeatherForecastParseError("longitude attribute in area tag not found") - - type = element.get("type") - if type is None: - raise WeatherForecastParseError("type attribute in area tag not found") - - region = element.get("region") - if region is None: - raise WeatherForecastParseError("region attribute in area tag not found") - - level = element.get("level") - if level is None: - raise WeatherForecastParseError("level attribute in area tag not found") - - description = element.get("description") - if description is None: - raise WeatherForecastParseError("description attribute in area tag not found") - - domain = element.get("domain") - if domain is None: - raise WeatherForecastParseError("domain attribute in area tag not found") - - tags = element.get("tags") - if tags is None: - raise WeatherForecastParseError("tags attribute in area tag not found") - - names = element.findall("name") - if len(names) < 2: - raise WeatherForecastParseError("one or more name tag in area tag not found") - - en_US = names[0].text - if en_US is None: - raise WeatherForecastParseError("name tag in area tag has no text") - - id_ID = names[1].text - if id_ID is None: - raise WeatherForecastParseError("name tag in area tag has no text") - - name = Name(en_US, id_ID) - - return Area( - id, - Coordinate(float(latitude), float(longitude)), - Type(type), - region, - level, - description, - domain, - tags, - name, - ) diff --git a/src/bmkg/parsers/parse_data_element.py b/src/bmkg/parsers/parse_data_element.py deleted file mode 100644 index 5fb88d9..0000000 --- a/src/bmkg/parsers/parse_data_element.py +++ /dev/null @@ -1,43 +0,0 @@ -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError -from ..schemas import Data - -__all__ = ["parse_data_element"] - - -def parse_data_element(element: Element) -> Data: - """ - Parse data element tree in xml. - - Args: - element: an element of data - - Returns: - A `Data` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ) - >>> data = parse_data_element(element) - >>> data - Data(source='meteofactory', productioncenter='NC Jakarta') - """ - source = element.get("source") - if source is None: - raise WeatherForecastParseError("source attribute in data tag not found") - - productioncenter = element.get("productioncenter") - if productioncenter is None: - raise WeatherForecastParseError( - "productioncenter attribute in data tag not found" - ) - - return Data(source, productioncenter) diff --git a/src/bmkg/parsers/parse_datetime_element.py b/src/bmkg/parsers/parse_datetime_element.py deleted file mode 100644 index 4e0f48e..0000000 --- a/src/bmkg/parsers/parse_datetime_element.py +++ /dev/null @@ -1,64 +0,0 @@ -from collections.abc import Iterator -from datetime import datetime - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError - -__all__ = ["parse_datetime_element"] - - -def parse_datetime_element(element: Element) -> Iterator[datetime]: - """ - Parse datetime element tree in xml. - - This parse datetime string found in element tree in the following format - `"%Y%m%d%H%M%S"`. - - Args: - element: any parameter element that contain datetime with type hourly - - Yields: - Some `datetime` object. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 5' - ... ' 5.75389725' - ... ' 9.26' - ... ' 2.57222222' - ... " " - ... ' ' - ... ' 2' - ... ' 2.3015589' - ... ' 3.704' - ... ' 1.028888888' - ... " " - ... ' ' - ... ' 0' - ... ' 0' - ... ' 0' - ... ' 0' - ... " " - ... "" - ... ) - >>> datetime = parse_datetime_element(element) - >>> datetime - - """ - for timerange in element: - dt = timerange.get("datetime") - if dt is None: - raise WeatherForecastParseError( - "datetime attribute in timerange tag not found" - ) - - yield datetime.strptime(dt, "%Y%m%d%H%M%S") diff --git a/src/bmkg/parsers/parse_forecast_element.py b/src/bmkg/parsers/parse_forecast_element.py deleted file mode 100644 index b7c4889..0000000 --- a/src/bmkg/parsers/parse_forecast_element.py +++ /dev/null @@ -1,35 +0,0 @@ -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError -from ..schemas import Forecast - -__all__ = ["parse_forecast_element"] - - -def parse_forecast_element(element: Element) -> Forecast: - """ - Parse forecast element tree in xml. - - Args: - element: an element of forecast - - Returns: - A `Forecast` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring('') - >>> forecast = parse_forecast_element(element) - >>> forecast - Forecast(domain='local') - """ - domain = element.get("domain") - if domain is None: - raise WeatherForecastParseError("domain attribute in forecast tag not found") - - return Forecast(domain) diff --git a/src/bmkg/parsers/parse_humidity_element.py b/src/bmkg/parsers/parse_humidity_element.py deleted file mode 100644 index 03ad096..0000000 --- a/src/bmkg/parsers/parse_humidity_element.py +++ /dev/null @@ -1,56 +0,0 @@ -from collections.abc import Iterator - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError -from ..schemas import Humidity - -__all__ = ["parse_humidity_element"] - - -def parse_humidity_element(element: Element) -> Iterator[Humidity]: - """ - Parse humidity element tree in xml. - - Args: - element: a parameter element that contain humidity - - Yields: - Some `Humidity` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 95' - ... " " - ... ' ' - ... ' 90' - ... " " - ... ' ' - ... ' 95' - ... " " - ... "" - ... ) - >>> humidity = parse_humidity_element(element) - >>> humidity - - """ - for timerange in element: - value_element = timerange.find("value") - if value_element is None: - raise WeatherForecastParseError( - "value tag in timerange tag not found" - ) from AttributeError - - humidity_text = value_element.text - if humidity_text is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - yield Humidity(int(humidity_text)) diff --git a/src/bmkg/parsers/parse_issue_element.py b/src/bmkg/parsers/parse_issue_element.py deleted file mode 100644 index eb279f2..0000000 --- a/src/bmkg/parsers/parse_issue_element.py +++ /dev/null @@ -1,92 +0,0 @@ -from datetime import datetime - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError - -__all__ = ["parse_issue_element"] - - -def parse_issue_element(element: Element) -> datetime: - """ - Parse issue element tree in xml. - - Args: - element: an element of issue - - Returns: - A naive `datetime` object. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... "" - ... " 20240116032347" - ... " 2024" - ... " 01" - ... " 16" - ... " 03" - ... " 23" - ... " 47" - ... "" - ... ) - >>> issue = parse_issue_element(element) - >>> issue - datetime.datetime(2024, 1, 16, 3, 23, 47) - """ - year_element = element.find("year") - if year_element is None: - raise WeatherForecastParseError("year tag in issue tag not found") - - year = year_element.text - if year is None: - raise WeatherForecastParseError("year tag in issue tag has no text") - - month_element = element.find("month") - if month_element is None: - raise WeatherForecastParseError("month tag in issue tag not found") - - month = month_element.text - if month is None: - raise WeatherForecastParseError("month tag in issue tag has no text") - - day_element = element.find("day") - if day_element is None: - raise WeatherForecastParseError("day tag in issue tag not found") - - day = day_element.text - if day is None: - raise WeatherForecastParseError("day tag in issue tag has no text") - - hour_element = element.find("hour") - if hour_element is None: - raise WeatherForecastParseError("hour tag in issue tag not found") - - hour = hour_element.text - if hour is None: - raise WeatherForecastParseError("hour tag in issue tag has no text") - - minute_element = element.find("minute") - if minute_element is None: - raise WeatherForecastParseError("minute tag in issue tag not found") - - minute = minute_element.text - if minute is None: - raise WeatherForecastParseError("minute tag in issue tag has no text") - - second_element = element.find("second") - if second_element is None: - raise WeatherForecastParseError("second tag in issue tag not found") - - second = second_element.text - if second is None: - raise WeatherForecastParseError("second tag in issue tag has no text") - - timestamp = year + month + day + hour + minute + second - - return datetime.strptime(timestamp, "%Y%m%d%H%M%S") diff --git a/src/bmkg/parsers/parse_location_data.py b/src/bmkg/parsers/parse_location_data.py new file mode 100644 index 0000000..75e8fdf --- /dev/null +++ b/src/bmkg/parsers/parse_location_data.py @@ -0,0 +1,30 @@ +from ..schemas import Location +from ..types import LocationData + +__all__ = ["parse_location_data"] + + +def parse_location_data(location_data: LocationData) -> Location: + """ + Parse `LocationData` JSON. + + Args: + location_data: A dict representing JSON of weather forecast data. + + Returns: + A `Location` schema. + """ + + return Location( + location_data["adm1"], + location_data["adm2"], + location_data["adm3"], + location_data["adm4"], + location_data["provinsi"], + location_data["kotkab"], + location_data["kecamatan"], + location_data["desa"], + float(location_data["lon"]), + float(location_data["lat"]), + location_data["timezone"], + ) diff --git a/src/bmkg/parsers/parse_temperature_element.py b/src/bmkg/parsers/parse_temperature_element.py deleted file mode 100644 index 1033272..0000000 --- a/src/bmkg/parsers/parse_temperature_element.py +++ /dev/null @@ -1,63 +0,0 @@ -from collections.abc import Iterator - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError -from ..schemas import Temperature - -__all__ = ["parse_temperature_element"] - - -def parse_temperature_element(element: Element) -> Iterator[Temperature]: - """ - Parse temperature element tree in xml. - - Args: - element: a parameter element that contain temperature - - Yields: - Some `Temperature` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 24' - ... ' 75.2' - ... " " - ... ' ' - ... ' 28' - ... ' 82.4' - ... " " - ... ' ' - ... ' 26' - ... ' 78.8' - ... " " - ... "" - ... ) - >>> temperature = parse_temperature_element(element) - >>> temperature - - """ - for timerange in element: - value_elements = timerange.findall("value") - if len(value_elements) < 2: - raise WeatherForecastParseError( - "one or more value tag in timerange tag not found" - ) - - celcius = value_elements[0].text - if celcius is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - fahrenheit = value_elements[1].text - if fahrenheit is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - yield Temperature(float(celcius), float(fahrenheit)) diff --git a/src/bmkg/parsers/parse_weather_data.py b/src/bmkg/parsers/parse_weather_data.py new file mode 100644 index 0000000..6c7f89e --- /dev/null +++ b/src/bmkg/parsers/parse_weather_data.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from .. import enums +from ..schemas import Weather +from ..types import WeatherData + +__all__ = ["parse_weather_data"] + + +def parse_weather_data(weather_data: WeatherData) -> Weather: + """ + Parse `WeatherData` JSON. + + Args: + weather_data: A dict representing JSON of weather forecast data. + + Returns: + A `Weather` schema. + """ + + return Weather( + datetime.fromisoformat(weather_data["datetime"]), + int(weather_data["t"]), + int(weather_data["tcc"]), + float(weather_data["tp"]), + enums.Weather(weather_data["weather"]), + int(weather_data["wd_deg"]), + enums.Cardinal(weather_data["wd"]), + enums.Cardinal(weather_data["wd_to"]), + float(weather_data["ws"]), + int(weather_data["hu"]), + int(weather_data["vs"]), + weather_data["time_index"], + datetime.fromisoformat(weather_data["analysis_date"]), + weather_data["image"], + datetime.strptime(weather_data["utc_datetime"], "%Y-%m-%d %H:%M:%S"), + datetime.strptime(weather_data["local_datetime"], "%Y-%m-%d %H:%M:%S"), + ) diff --git a/src/bmkg/parsers/parse_weather_element.py b/src/bmkg/parsers/parse_weather_element.py deleted file mode 100644 index 684399b..0000000 --- a/src/bmkg/parsers/parse_weather_element.py +++ /dev/null @@ -1,54 +0,0 @@ -from collections.abc import Iterator - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from .. import enums -from ..exceptions import WeatherForecastParseError - -__all__ = ["parse_weather_element"] - - -def parse_weather_element(element: Element) -> Iterator[enums.Weather]: - """ - Parse weather element tree in xml. - - Args: - element: a parameter element that contain weather - - Yields: - Some `Weather` enum. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 60' - ... " " - ... ' ' - ... ' 60' - ... " " - ... ' ' - ... ' 1' - ... " " - ... "" - ... ) - >>> weather = parse_weather_element(element) - >>> weather - - """ - for timerange in element: - value_elements = timerange.find("value") - if value_elements is None: - raise WeatherForecastParseError("value tag in timerange tag not found") - - weather = value_elements.text - if weather is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - yield enums.Weather(int(weather)) diff --git a/src/bmkg/parsers/parse_weather_forecast_data.py b/src/bmkg/parsers/parse_weather_forecast_data.py index d04e61d..27e637c 100644 --- a/src/bmkg/parsers/parse_weather_forecast_data.py +++ b/src/bmkg/parsers/parse_weather_forecast_data.py @@ -1,130 +1,27 @@ -from collections.abc import Iterator -from itertools import chain -from typing import Any - -from defusedxml.ElementTree import fromstring - -from ..enums import Type -from ..exceptions import WeatherForecastParseError -from ..schemas import ( - Weather, - WeatherForecast, -) -from ..types import WeatherForecastParameter -from .parse_area_element import parse_area_element -from .parse_data_element import parse_data_element -from .parse_datetime_element import parse_datetime_element -from .parse_forecast_element import parse_forecast_element -from .parse_humidity_element import parse_humidity_element -from .parse_issue_element import parse_issue_element -from .parse_temperature_element import parse_temperature_element -from .parse_weather_element import parse_weather_element -from .parse_wind_direction_element import parse_wind_direction_element -from .parse_wind_speed_element import parse_wind_speed_element +from ..schemas import WeatherForecast +from ..types import WeatherForecastData +from .parse_location_data import parse_location_data +from .parse_weather_data import parse_weather_data __all__ = ["parse_weather_forecast_data"] -def parse_weather_forecast_data(weather_forecast_data: str | bytes) -> WeatherForecast: +def parse_weather_forecast_data( + weather_forecast_data: WeatherForecastData, +) -> WeatherForecast: """ - Parse weather forecast data element tree in xml. - - This control when area type is sea then it has no information regarding weather - forecast. Also this use wind speed element tree to get the datetime information. + Parse `WeatherForecastData` JSON. Args: - weather_forecast_data: string or bytes of xml data. + weather_forecast_data: A dict representing JSON of weather forecast data. Returns: A `WeatherForecast` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. """ - root = fromstring(weather_forecast_data) - - data = parse_data_element(root) - - forecast_element = root[0] - forecast = parse_forecast_element(forecast_element) - - issue_element = forecast_element[0] - issue = parse_issue_element(issue_element) - - areas = {} - for area_element in forecast_element.iterfind("area"): - area = parse_area_element(area_element) - # if the area type is sea, then the weather is empty - weathers: Iterator[Weather] | None = None - - # Only land that has weather, sea doesn't - if area.type == Type.LAND: - parameters: dict[str, Any] = {} - for parameter_element in area_element.iterfind("parameter"): - parameter_id = parameter_element.get("id") - if parameter_id is None: - raise WeatherForecastParseError( - "id attribute in parameter tag not found" - ) - - parameter: WeatherForecastParameter - if ( - parameter_id == "hu" - or parameter_id == "humax" - or parameter_id == "humin" - ): - parameter = parse_humidity_element(parameter_element) - elif ( - parameter_id == "t" - or parameter_id == "tmax" - or parameter_id == "tmin" - ): - parameter = parse_temperature_element(parameter_element) - elif parameter_id == "weather": - parameter = parse_weather_element(parameter_element) - elif parameter_id == "wd": - parameter = parse_wind_direction_element(parameter_element) - else: - # parameter_id == "ws": - parameter = parse_wind_speed_element(parameter_element) - - # Choose one of parameter tag that has datetime attribute with - # type hourly. Strictly speaking it is timerange tag. - parameters["datetime"] = parse_datetime_element(parameter_element) - - parameters[parameter_id] = parameter - - # The length of the data to be zipped is 12. This is because the weather - # forecast data received is for three days, each receiving 4 data. However, - # for tmin, tmax, hmin, and hmax data, these four data are only given 1 each - # for that day, therefore there are only 3 data for 3 days from tmin, tmax, - # hmin, and hmax. Because the zip() function will stop at the smallest data - # length, therefore we need to multiply the 3 data by 4 to get 12 - weathers = ( - Weather(dt, weather, t, tmix, tmax, h, hmin, hmax, wd, ws) - for dt, weather, t, tmix, tmax, h, hmin, hmax, wd, ws in zip( - parameters["datetime"], - parameters["weather"], - parameters["t"], - chain.from_iterable( - [(val, val, val, val) for val in parameters["tmin"]] - ), - chain.from_iterable( - [(val, val, val, val) for val in parameters["tmax"]] - ), - parameters["hu"], - chain.from_iterable( - [(val, val, val, val) for val in parameters["humin"]] - ), - chain.from_iterable( - [(val, val, val, val) for val in parameters["humax"]] - ), - parameters["wd"], - parameters["ws"], - strict=False, - ) - ) - - areas[area] = weathers + location = parse_location_data(weather_forecast_data["lokasi"]) + weathers = [ + parse_weather_data(weather) + for weather in weather_forecast_data["data"][0]["cuaca"][0] + ] - return WeatherForecast(data, forecast, issue, areas) + return WeatherForecast(location, weathers) diff --git a/src/bmkg/parsers/parse_wind_direction_element.py b/src/bmkg/parsers/parse_wind_direction_element.py deleted file mode 100644 index fc4a9e5..0000000 --- a/src/bmkg/parsers/parse_wind_direction_element.py +++ /dev/null @@ -1,71 +0,0 @@ -from collections.abc import Iterator - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..enums import Cardinal, Sexa -from ..exceptions import WeatherForecastParseError -from ..schemas import WindDirection - -__all__ = ["parse_wind_direction_element"] - - -def parse_wind_direction_element(element: Element) -> Iterator[WindDirection]: - """ - Parse wind direction element tree in xml. - - Args: - element: a parameter element that contain wind direction - - Yields: - Some `WindDirection` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 90' - ... ' E' - ... ' 9000' - ... " " - ... ' ' - ... ' 157.5' - ... ' SSE' - ... ' 15730' - ... " " - ... ' ' - ... ' 0' - ... ' VARIABLE' - ... ' 000' - ... " " - ... "" - ... ) - >>> wind_direction = parse_wind_direction_element(element) - >>> wind_direction - - """ - for timerange in element: - value_elements = timerange.findall("value") - if len(value_elements) < 3: - raise WeatherForecastParseError( - "one or more value tag in timerange tag not found" - ) - - deg = value_elements[0].text - if deg is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - card = value_elements[1].text - if card is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - sexa = value_elements[2].text - if sexa is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - yield WindDirection(float(deg), Cardinal(card), Sexa(sexa)) diff --git a/src/bmkg/parsers/parse_wind_speed_element.py b/src/bmkg/parsers/parse_wind_speed_element.py deleted file mode 100644 index 95481d3..0000000 --- a/src/bmkg/parsers/parse_wind_speed_element.py +++ /dev/null @@ -1,77 +0,0 @@ -from collections.abc import Iterator - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -from ..exceptions import WeatherForecastParseError -from ..schemas import WindSpeed - -__all__ = ["parse_wind_speed_element"] - - -def parse_wind_speed_element(element: Element) -> Iterator[WindSpeed]: - """ - Parse wind speed element tree in xml. - - Args: - element: a parameter element that contain wind speed - - Yields: - Some `WindSpeed` schema. - - Raises: - WeatherForecastParseError: If some expected attribute is not found. - - Examples: - >>> from defusedxml.ElementTree import fromstring - >>> element = fromstring( - ... '' - ... ' ' - ... ' 5' - ... ' 5.75389725' - ... ' 9.26' - ... ' 2.57222222' - ... " " - ... ' ' - ... ' 2' - ... ' 2.3015589' - ... ' 3.704' - ... ' 1.028888888' - ... " " - ... ' ' - ... ' 0' - ... ' 0' - ... ' 0' - ... ' 0' - ... " " - ... "" - ... ) - >>> wind_speed = parse_wind_speed_element(element) - >>> wind_speed - - """ - for timerange in element: - value_elements = timerange.findall("value") - if len(value_elements) < 4: - raise WeatherForecastParseError( - "one or more value tag in timerange tag not found" - ) - - knot = value_elements[0].text - if knot is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - mph = value_elements[1].text - if mph is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - kph = value_elements[2].text - if kph is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - ms = value_elements[3].text - if ms is None: - raise WeatherForecastParseError("value tag in timerange tag has no text") - - yield WindSpeed(float(knot), float(mph), float(kph), float(ms)) diff --git a/src/bmkg/schemas/__init__.py b/src/bmkg/schemas/__init__.py index e370df9..3531102 100644 --- a/src/bmkg/schemas/__init__.py +++ b/src/bmkg/schemas/__init__.py @@ -1,35 +1,21 @@ -from .area import Area from .coordinate import Coordinate -from .data import Data from .earthquake import Earthquake from .felt_earthquake import FeltEarthquake -from .forecast import Forecast -from .humidity import Humidity from .latest_earthquake import LatestEarthquake -from .name import Name +from .location import Location from .shakemap import Shakemap from .strong_earthquake import StrongEarthquake -from .temperature import Temperature from .weather import Weather from .weather_forecast import WeatherForecast -from .wind_direction import WindDirection -from .wind_speed import WindSpeed __all__ = [ - "Area", "Coordinate", - "Data", "Earthquake", "FeltEarthquake", - "Forecast", - "Humidity", "LatestEarthquake", - "Name", + "Location", "Shakemap", "StrongEarthquake", - "Temperature", - "WeatherForecast", "Weather", - "WindDirection", - "WindSpeed", + "WeatherForecast", ] diff --git a/src/bmkg/schemas/area.py b/src/bmkg/schemas/area.py deleted file mode 100644 index 1f37802..0000000 --- a/src/bmkg/schemas/area.py +++ /dev/null @@ -1,35 +0,0 @@ -from dataclasses import dataclass - -from ..enums import Type -from .coordinate import Coordinate -from .name import Name - -__all__ = ["Area"] - - -@dataclass(unsafe_hash=True, slots=True) -class Area: - """ - A schema used to store info about area. - - Attributes: - id: id of an area. - coordinate: coordinate of an area. - type: type of an area. - region: region of an area. - level: level of an area. - description: description of an area. - domain: domain of an area. - tags: tags of an area. - names: names of an area. - """ - - id: str - coordinate: Coordinate - type: Type - region: str - level: str - description: str - domain: str - tags: str - names: Name diff --git a/src/bmkg/schemas/data.py b/src/bmkg/schemas/data.py deleted file mode 100644 index b401edb..0000000 --- a/src/bmkg/schemas/data.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass - -__all__ = ["Data"] - - -@dataclass(slots=True) -class Data: - """ - A schema used to store info about data. - - Attributes: - source: source of a data. - productioncenter: production center of a data. - """ - - source: str - productioncenter: str diff --git a/src/bmkg/schemas/forecast.py b/src/bmkg/schemas/forecast.py deleted file mode 100644 index e69f291..0000000 --- a/src/bmkg/schemas/forecast.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass - -__all__ = ["Forecast"] - - -@dataclass(slots=True) -class Forecast: - """ - A schema used to store info about forecast. - - Attributes: - domain: domain of a forecast. - """ - - domain: str diff --git a/src/bmkg/schemas/humidity.py b/src/bmkg/schemas/humidity.py deleted file mode 100644 index 0a6b17d..0000000 --- a/src/bmkg/schemas/humidity.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass, field - -__all__ = ["Humidity"] - - -@dataclass(slots=True) -class Humidity: - """ - A schema used to store info about humidity. - - Attributes: - percentage: percentage of humidity in % as its unit. - """ - - percentage: int = field(metadata={"unit": "%"}) diff --git a/src/bmkg/schemas/location.py b/src/bmkg/schemas/location.py new file mode 100644 index 0000000..27cb324 --- /dev/null +++ b/src/bmkg/schemas/location.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass + +__all__ = ["Location"] + + +@dataclass(slots=True) +class Location: + """ + A schema used to store information about a geographic location. + + Attributes: + admin_level_1: The administrative level 1 code, typically representing the province. + admin_level_2: The administrative level 2 code, typically representing the city or district. + admin_level_3: The administrative level 3 code, typically representing the subdistrict. + admin_level_4: The administrative level 4 code, typically representing a more localized area (e.g., village or neighborhood). + province: The name of the province or state. + city: The name of the city or district. + subdistrict: The name of the subdistrict (a subdivision of a city or district). + village: The name of the village or rural area. + longitude: The geographic longitude coordinate of the location. + latitude: The geographic latitude coordinate of the location. + timezone: The time zone identifier for the location, e.g., "Asia/Jakarta". + """ + + admin_level_1: str + admin_level_2: str + admin_level_3: str + admin_level_4: str + province: str + city: str + subdistrict: str + village: str + longitude: float + latitude: float + timezone: str diff --git a/src/bmkg/schemas/name.py b/src/bmkg/schemas/name.py deleted file mode 100644 index 452395f..0000000 --- a/src/bmkg/schemas/name.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass - -__all__ = ["Name"] - - -@dataclass(unsafe_hash=True, slots=True) -class Name: - """ - A schema used to store info about name of an area. - - Attributes: - en_US: name of an area in english. - id_ID: name of an area in indonesian. - """ - - en_US: str - id_ID: str diff --git a/src/bmkg/schemas/temperature.py b/src/bmkg/schemas/temperature.py deleted file mode 100644 index 1c9bd73..0000000 --- a/src/bmkg/schemas/temperature.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass, field - -__all__ = ["Temperature"] - - -@dataclass(slots=True) -class Temperature: - """ - A schema used to store info about temperature. - - Attributes: - celcius: temperature in celcius with C as its unit. - fahrenheit: temperature in fahrenheit with F as its unit. - """ - - celcius: float = field(metadata={"unit": "C"}) - fahrenheit: float = field(metadata={"unit": "F"}) diff --git a/src/bmkg/schemas/weather.py b/src/bmkg/schemas/weather.py index 5ef68a2..c6df075 100644 --- a/src/bmkg/schemas/weather.py +++ b/src/bmkg/schemas/weather.py @@ -1,11 +1,7 @@ +import datetime as dt from dataclasses import dataclass -from datetime import datetime -from ..enums import Weather as WeatherEnum -from .humidity import Humidity -from .temperature import Temperature -from .wind_direction import WindDirection -from .wind_speed import WindSpeed +from .. import enums __all__ = ["Weather"] @@ -13,33 +9,44 @@ @dataclass(slots=True) class Weather: """ - A schema used to store info about weather. + A schema used to store information about a weather. Attributes: - datetime: datetime of a weather. - weather: `Weather` enum symbolic names (members) of a weather condition. - temperature: temperature of a weather. - minimum_temperature: minimum temperature of a weather. - maximum_temperature: maximum temperature of a weather. - humidity: humidity of a weather. - min_humidity: minimum humidity of a weather. - max_humidity: maximum humidity of a weather. - wind_direction: wind direction of a weather. - wind_speed: wind speed of a weather. - - Note: - `datetime` is naive datetime, means it has no information about its timezone. - Also don't be confused with `weather`, this `weather` field is weather enum that - has representation of weather condition. + datetime: The datetime of the weather data in UTC format. + t: Temperature in degrees Celsius. + tcc: Total cloud cover percentage (0-100%). + tp: Precipitation amount in millimeters. + weather: Weather condition code, corresponds to a predefined set of + weather conditions. + wd_deg: Wind direction in degrees (0-360), where 0° is North. + wd: Wind direction from which the wind blows. + wd_to: Wind direction the wind is blowing towards. + ws: Wind speed in kilometers per hour (km/h). + hu: Humidity percentage (0-100%). + vs: Visibility in meters. + time_index: Time index in the format `"x-y"`, where `x` is the hour + start of the forecast, and `y` is the hour end of the forecast. + analysis_date: The date when the weather data was generated (in UTC + format). + image: URL to an image representing the weather condition (e.g., an + icon). + utc_datetime: The UTC datetime when the weather data was recorded. + local_datetime: The local datetime when the weather data was recorded in this format "%Y-%m-%d %H:%M:%S"(adjusted for time zone). """ - datetime: datetime - weather: WeatherEnum - temperature: Temperature - minimum_temperature: Temperature - maximum_temperature: Temperature - humidity: Humidity - min_humidity: Humidity - max_humidity: Humidity - wind_direction: WindDirection - wind_speed: WindSpeed + datetime: dt.datetime + t: int + tcc: int + tp: float + weather: enums.Weather + wd_deg: int + wd: enums.Cardinal + wd_to: enums.Cardinal + ws: float + hu: int + vs: int + time_index: str + analysis_date: dt.datetime + image: str + utc_datetime: dt.datetime + local_datetime: dt.datetime diff --git a/src/bmkg/schemas/weather_forecast.py b/src/bmkg/schemas/weather_forecast.py index 955add8..2743a8a 100644 --- a/src/bmkg/schemas/weather_forecast.py +++ b/src/bmkg/schemas/weather_forecast.py @@ -1,10 +1,6 @@ -from collections.abc import Iterator from dataclasses import dataclass -from datetime import datetime -from .area import Area -from .data import Data -from .forecast import Forecast +from .location import Location from .weather import Weather __all__ = ["WeatherForecast"] @@ -13,22 +9,12 @@ @dataclass(slots=True) class WeatherForecast: """ - A schema used to store info about weather forecast. + A schema used to store information about weather forecast. Attributes: - data: Data schema from api. - forecast: Forecast schema from api. - issue: datetime of the issue. - weathers: weather info with Area as key and iterator of Weather as value. - - Note: - `issue` field is naive datetime, means it has no information about its - timezone. `weathers` has an `Area` schema as it key and iterator of `Weather` - that contain information about weather forecast of that area as it value. Only - area type is land that has weather forecast information, whereas sea is not. + location: location information. + weathers: list of weather forecast data. """ - data: Data - forecast: Forecast - issue: datetime - weathers: dict[Area, Iterator[Weather] | None] + location: Location + weathers: list[Weather] diff --git a/src/bmkg/schemas/wind_direction.py b/src/bmkg/schemas/wind_direction.py deleted file mode 100644 index c99bd2b..0000000 --- a/src/bmkg/schemas/wind_direction.py +++ /dev/null @@ -1,21 +0,0 @@ -from dataclasses import dataclass, field - -from ..enums import Cardinal, Sexa - -__all__ = ["WindDirection"] - - -@dataclass(slots=True) -class WindDirection: - """ - A schema used to store info about wind direction. - - Attributes: - deg: degree of a wind direction with deg as its unit. - card: card direction of a wind direction with CARD as its unit. - sexa: sexa direction of a wind direction with SEXA as its unit. - """ - - deg: float = field(metadata={"unit": "deg"}) - card: Cardinal = field(metadata={"unit": "CARD"}) - sexa: Sexa = field(metadata={"unit": "SEXA"}) diff --git a/src/bmkg/schemas/wind_speed.py b/src/bmkg/schemas/wind_speed.py deleted file mode 100644 index eaccf01..0000000 --- a/src/bmkg/schemas/wind_speed.py +++ /dev/null @@ -1,21 +0,0 @@ -from dataclasses import dataclass, field - -__all__ = ["WindSpeed"] - - -@dataclass(slots=True) -class WindSpeed: - """ - A schema used to store info about wind speed. - - Attributes: - knot: knot of a wind speed with kn as its unit. - mph: mph of a wind speed with mph as its unit. - kph: kph of a wind speed with km/h as its unit. - ms: ms of a wind speed with m/s as its unit. - """ - - knot: float = field(metadata={"unit": "kn"}) - mph: float = field(metadata={"unit": "mph"}) - kph: float = field(metadata={"unit": "km/h"}) - ms: float = field(metadata={"unit": "m/s"}) diff --git a/src/bmkg/types.py b/src/bmkg/types.py index 03ba329..4e9aec8 100644 --- a/src/bmkg/types.py +++ b/src/bmkg/types.py @@ -1,16 +1,6 @@ -from datetime import datetime -from typing import Iterator, TypedDict - -from .enums import Weather -from .schemas import ( - Humidity, - Temperature, - WindDirection, - WindSpeed, -) +from typing import TypedDict __all__ = [ - "WeatherForecastParameter", "EarthquakeData", "FeltEarthquakeData", "LatestEarthquakeData", @@ -18,19 +8,13 @@ "InfoFeltEarthquakeData", "InfoLatestEarthquakeData", "InfoStrongEarthquakeData", + "LocationData", + "OptionalLocationData", + "WeatherData", + "WeatherForecastData", ] -WeatherForecastParameter = ( - Iterator[datetime] - | Iterator[Humidity] - | Iterator[Temperature] - | Iterator[Weather] - | Iterator[WindDirection] - | Iterator[WindSpeed] -) - - class EarthquakeData(TypedDict): Tanggal: str Jam: str @@ -79,3 +63,53 @@ class _LatestEarthquakeData(TypedDict): class InfoLatestEarthquakeData(TypedDict): Infogempa: _LatestEarthquakeData + + +class LocationData(TypedDict): + adm1: str + adm2: str + adm3: str + adm4: str + provinsi: str + kotkab: str + kecamatan: str + desa: str + lon: float + lat: float + timezone: str + + +class OptionalLocationData(LocationData): + type: str + + +class WeatherData(TypedDict): + datetime: str + t: int + tcc: int + tp: float + weather: int + weather_desc: str + weather_desc_en: str + wd_deg: int + wd: str + wd_to: str + ws: float + hu: int + vs: int + vs_text: str + time_index: str + analysis_date: str + image: str + utc_datetime: str + local_datetime: str + + +class _WeatherForecastData(TypedDict): + lokasi: OptionalLocationData + cuaca: list[list[WeatherData]] + + +class WeatherForecastData(TypedDict): + lokasi: LocationData + data: list[_WeatherForecastData] diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..484a486 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,18 @@ +import pytest +from aiohttp import ClientSession + +from bmkg.api.api import API + + +async def test_api_without_base_url(): + with pytest.raises(NotImplementedError): + API() + + +async def test_api_with_base_url(): + class TestAPI(API): + base_url = "http://example.com" + + api = TestAPI(session=ClientSession()) + + assert api.base_url == "http://example.com" diff --git a/tests/test_integration/test_earthquake_integration.py b/tests/test_integration/test_earthquake_integration.py index 4248874..06def79 100644 --- a/tests/test_integration/test_earthquake_integration.py +++ b/tests/test_integration/test_earthquake_integration.py @@ -1,10 +1,4 @@ -from bmkg import BMKG -from bmkg.api import Earthquake, Shakemap - - -async def test_when_request_latest_earthquake_then_pass(): - async with BMKG() as bmkg: - await bmkg.earthquake.get_latest_earthquake() +from bmkg import Earthquake, Shakemap async def test_when_direct_request_latest_earthquake_then_pass(): @@ -12,32 +6,16 @@ async def test_when_direct_request_latest_earthquake_then_pass(): await earthquake.get_latest_earthquake() -async def test_when_request_latest_earthquake_shakemap_then_pass(): - async with BMKG() as bmkg: - latest_earthquake = await bmkg.earthquake.get_latest_earthquake() - await latest_earthquake.shakemap.get_content() - - async def test_when_direct_request_latest_earthquake_shakemap_then_pass(): async with Shakemap("20240203152510.mmi.jpg") as shakemap: await shakemap.get_content() -async def test_when_request_strong_earthquake_then_pass(): - async with BMKG() as bmkg: - list(await bmkg.earthquake.get_strong_earthquake()) - - async def test_when_direct_request_strong_earthquake_then_pass(): async with Earthquake() as earthquake: list(await earthquake.get_strong_earthquake()) -async def test_when_request_felt_earthquake_then_pass(): - async with BMKG() as bmkg: - list(await bmkg.earthquake.get_felt_earthquake()) - - async def test_when_direct_request_felt_earthquake_then_pass(): async with Earthquake() as earthquake: list(await earthquake.get_felt_earthquake()) diff --git a/tests/test_integration/test_weather_forecast_integration.py b/tests/test_integration/test_weather_forecast_integration.py index 7367b71..0337e59 100644 --- a/tests/test_integration/test_weather_forecast_integration.py +++ b/tests/test_integration/test_weather_forecast_integration.py @@ -1,25 +1,6 @@ -import pytest +from bmkg import WeatherForecast -from bmkg import BMKG -from bmkg.api import WeatherForecast -from bmkg.enums import Province, Type - -@pytest.mark.parametrize("province", Province) -async def test_given_provinces_when_request_weather_forecast_then_pass(province): - async with BMKG() as bmkg: - weather_forecast_data = await bmkg.weather_forecast.get_weather_forecast( - province - ) - for area, weather in weather_forecast_data.weathers.items(): - if area.type == Type.LAND: - list(weather) - - -@pytest.mark.parametrize("province", Province) -async def test_given_provinces_when_direct_request_weather_forecast_then_pass(province): +async def test_given_provinces_when_direct_request_weather_forecast_then_pass(): async with WeatherForecast() as weather_forecast: - weather_forecast_data = await weather_forecast.get_weather_forecast(province) - for area, weather in weather_forecast_data.weathers.items(): - if area.type == Type.LAND: - list(weather) + await weather_forecast.get_weather_forecast("11.01.01.2001") diff --git a/tests/test_parsers/__init__.py b/tests/test_parsers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_parsers/test_parse_area_element.py b/tests/test_parsers/test_parse_area_element.py deleted file mode 100644 index f0ae97d..0000000 --- a/tests/test_parsers/test_parse_area_element.py +++ /dev/null @@ -1,62 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_area_element - - -@pytest.mark.parametrize( - "attr, err_msg", - ( - ("id", "id attribute in area tag not found"), - ("latitude", "latitude attribute in area tag not found"), - ("longitude", "longitude attribute in area tag not found"), - ("type", "type attribute in area tag not found"), - ("region", "region attribute in area tag not found"), - ("level", "level attribute in area tag not found"), - ("description", "description attribute in area tag not found"), - ("domain", "domain attribute in area tag not found"), - ("tags", "tags attribute in area tag not found"), - ), -) -def test_parse_element_with_invalid_attribute(attr, err_msg): - element = MagicMock() - element.get.side_effect = lambda x: None if x == attr else MagicMock() - - with pytest.raises(WeatherForecastParseError, match=err_msg): - parse_area_element(element) - - -def test_parse_element_with_wrong_names_length(): - element = MagicMock() - element.findall.__len__.return_value = 1 - - with pytest.raises( - WeatherForecastParseError, match="one or more name tag in area tag not found" - ): - parse_area_element(element) - - -@pytest.mark.parametrize( - "index, err_msg", - ( - (0, "name tag in area tag has no text"), - (1, "name tag in area tag has no text"), - ), -) -def test_parse_element_with_invalid_en_US_name(index, err_msg): - value_element = MagicMock() - value_element.text = None - - value_elements = MagicMock() - value_elements.__len__.return_value = 2 - value_elements.__getitem__.side_effect = ( - lambda idx: value_element if idx == index else MagicMock() - ) - - element = MagicMock() - element.findall.return_value = value_elements - - with pytest.raises(WeatherForecastParseError, match=err_msg): - parse_area_element(element) diff --git a/tests/test_parsers/test_parse_data_element.py b/tests/test_parsers/test_parse_data_element.py deleted file mode 100644 index 721efcc..0000000 --- a/tests/test_parsers/test_parse_data_element.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_data_element - - -@pytest.mark.parametrize( - "attr, err_msg", - ( - ("source", "source attribute in data tag not found"), - ("productioncenter", "productioncenter attribute in data tag not found"), - ), -) -def test_parse_element_with_invalid_attribute(attr, err_msg): - element = MagicMock() - element.get.side_effect = lambda x: None if x == attr else MagicMock() - - with pytest.raises(WeatherForecastParseError, match=err_msg): - parse_data_element(element) diff --git a/tests/test_parsers/test_parse_datetime_element.py b/tests/test_parsers/test_parse_datetime_element.py deleted file mode 100644 index 1ca5911..0000000 --- a/tests/test_parsers/test_parse_datetime_element.py +++ /dev/null @@ -1,19 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_datetime_element - - -def test_parse_element_with_invalid_attribute(): - timerange = MagicMock() - timerange.get.return_value = None - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, match="datetime attribute in timerange tag not found" - ): - for _dt in parse_datetime_element(element): - pass diff --git a/tests/test_parsers/test_parse_forecast_element.py b/tests/test_parsers/test_parse_forecast_element.py deleted file mode 100644 index 45897de..0000000 --- a/tests/test_parsers/test_parse_forecast_element.py +++ /dev/null @@ -1,16 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_forecast_element - - -def test_parse_element_with_invalid_attribute(): - element = MagicMock() - element.get.return_value = None - - with pytest.raises( - WeatherForecastParseError, match="domain attribute in forecast tag not found" - ): - parse_forecast_element(element) diff --git a/tests/test_parsers/test_parse_humidity_element.py b/tests/test_parsers/test_parse_humidity_element.py deleted file mode 100644 index 91108d4..0000000 --- a/tests/test_parsers/test_parse_humidity_element.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_humidity_element - - -def test_parse_element_with_invalid_attribute(): - timerange = MagicMock() - timerange.find.return_value = None - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, match="value tag in timerange tag not found" - ): - for _humidity in parse_humidity_element(element): - pass - - -def test_parse_element_with_invalid_humidity_text(): - humidity = MagicMock() - humidity.text = None - - timerange = MagicMock() - timerange.find.return_value = humidity - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, match="value tag in timerange tag has no text" - ): - for _humidity in parse_humidity_element(element): - pass diff --git a/tests/test_parsers/test_parse_issue_element.py b/tests/test_parsers/test_parse_issue_element.py deleted file mode 100644 index a53274c..0000000 --- a/tests/test_parsers/test_parse_issue_element.py +++ /dev/null @@ -1,47 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_issue_element - - -@pytest.mark.parametrize( - "tag, err_msg", - ( - ("year", "year tag in issue tag not found"), - ("month", "month tag in issue tag not found"), - ("day", "day tag in issue tag not found"), - ("hour", "hour tag in issue tag not found"), - ("minute", "minute tag in issue tag not found"), - ("second", "second tag in issue tag not found"), - ), -) -def test_parse_element_with_invalid_attribute(tag, err_msg): - element = MagicMock() - element.find.side_effect = lambda x: None if x == tag else MagicMock() - - with pytest.raises(WeatherForecastParseError, match=err_msg): - parse_issue_element(element) - - -@pytest.mark.parametrize( - "tag, err_msg", - ( - ("year", "year tag in issue tag has no text"), - ("month", "month tag in issue tag has no text"), - ("day", "day tag in issue tag has no text"), - ("hour", "hour tag in issue tag has no text"), - ("minute", "minute tag in issue tag has no text"), - ("second", "second tag in issue tag has no text"), - ), -) -def test_parse_element_with_invalid_timestamp_text(tag, err_msg): - timestamp = MagicMock() - timestamp.text = None - - element = MagicMock() - element.find.side_effect = lambda x: timestamp if x == tag else MagicMock() - - with pytest.raises(WeatherForecastParseError, match=err_msg): - parse_issue_element(element) diff --git a/tests/test_parsers/test_parse_temperature_element.py b/tests/test_parsers/test_parse_temperature_element.py deleted file mode 100644 index b0efc99..0000000 --- a/tests/test_parsers/test_parse_temperature_element.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_temperature_element - - -def test_parse_element_with_invalid_attribute(): - value_elements = MagicMock() - value_elements.__len__.return_value = 1 - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match="one or more value tag in timerange tag not found", - ): - for _temperature in parse_temperature_element(element): - pass - - -@pytest.mark.parametrize( - "index, err_msg", - ( - (0, "value tag in timerange tag has no text"), - (1, "value tag in timerange tag has no text"), - ), -) -def test_parse_element_with_invalid_value_elements_text(index, err_msg): - value_element = MagicMock() - value_element.text = None - - value_elements = MagicMock() - value_elements.__len__.return_value = 2 - value_elements.__getitem__.side_effect = ( - lambda idx: value_element if idx == index else MagicMock() - ) - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match=err_msg, - ): - for _temperature in parse_temperature_element(element): - pass diff --git a/tests/test_parsers/test_parse_weather_element.py b/tests/test_parsers/test_parse_weather_element.py deleted file mode 100644 index 4bef1fb..0000000 --- a/tests/test_parsers/test_parse_weather_element.py +++ /dev/null @@ -1,35 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_weather_element - - -def test_parse_element_with_invalid_attribute(): - timerange = MagicMock() - timerange.find.return_value = None - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, match="value tag in timerange tag not found" - ): - for _weather in parse_weather_element(element): - pass - - -def test_parse_element_with_invalid_weather_text(): - weather = MagicMock() - weather.text = None - - timerange = MagicMock() - timerange.find.return_value = weather - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, match="value tag in timerange tag has no text" - ): - for _weather in parse_weather_element(element): - pass diff --git a/tests/test_parsers/test_parse_weather_forecast_data.py b/tests/test_parsers/test_parse_weather_forecast_data.py deleted file mode 100644 index 6671305..0000000 --- a/tests/test_parsers/test_parse_weather_forecast_data.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_weather_forecast_data - - -def test_parse_element_with_invalid_attribute(): - with pytest.raises( - WeatherForecastParseError, match="id attribute in parameter tag not found" - ): - parse_weather_forecast_data( - '' - " " - " " - " 20240216023745" - " 2024" - " 02" - " 16" - " 02" - " 37" - " 45" - " " - " " - ' Aceh Barat' - ' Kab. Aceh Barat' - ' ' - " " - " " - " " - "" - ) diff --git a/tests/test_parsers/test_parse_wind_direction_element.py b/tests/test_parsers/test_parse_wind_direction_element.py deleted file mode 100644 index 80dca62..0000000 --- a/tests/test_parsers/test_parse_wind_direction_element.py +++ /dev/null @@ -1,54 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_wind_direction_element - - -def test_parse_element_with_invalid_attribute(): - value_elements = MagicMock() - value_elements.__len__.return_value = 2 - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match="one or more value tag in timerange tag not found", - ): - for _wind_direction in parse_wind_direction_element(element): - pass - - -@pytest.mark.parametrize( - "index, err_msg", - ( - (0, "value tag in timerange tag has no text"), - (1, "value tag in timerange tag has no text"), - (2, "value tag in timerange tag has no text"), - ), -) -def test_parse_element_with_invalid_value_elements_text(index, err_msg): - value_element = MagicMock() - value_element.text = None - - value_elements = MagicMock() - value_elements.__len__.return_value = 3 - value_elements.__getitem__.side_effect = ( - lambda idx: value_element if idx == index else MagicMock() - ) - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match=err_msg, - ): - for _wind_direction in parse_wind_direction_element(element): - pass diff --git a/tests/test_parsers/test_parse_wind_speed_element.py b/tests/test_parsers/test_parse_wind_speed_element.py deleted file mode 100644 index cff6580..0000000 --- a/tests/test_parsers/test_parse_wind_speed_element.py +++ /dev/null @@ -1,55 +0,0 @@ -from unittest.mock import MagicMock - -import pytest - -from bmkg.exceptions import WeatherForecastParseError -from bmkg.parsers import parse_wind_speed_element - - -def test_parse_element_with_invalid_attribute(): - value_elements = MagicMock() - value_elements.__len__.return_value = 3 - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match="one or more value tag in timerange tag not found", - ): - for _wind_speed in parse_wind_speed_element(element): - pass - - -@pytest.mark.parametrize( - "index, err_msg", - ( - (0, "value tag in timerange tag has no text"), - (1, "value tag in timerange tag has no text"), - (2, "value tag in timerange tag has no text"), - (3, "value tag in timerange tag has no text"), - ), -) -def test_parse_element_with_invalid_value_elements_text(index, err_msg): - value_element = MagicMock() - value_element.text = None - - value_elements = MagicMock() - value_elements.__len__.return_value = 4 - value_elements.__getitem__.side_effect = ( - lambda idx: value_element if idx == index else MagicMock() - ) - - timerange = MagicMock() - timerange.findall.return_value = value_elements - - element = MagicMock() - element.__iter__.return_value = [timerange] - with pytest.raises( - WeatherForecastParseError, - match=err_msg, - ): - for _wind_speed in parse_wind_speed_element(element): - pass