From 28d9ae9a46bb1487ac2c0e06dba35fd9e31466ab Mon Sep 17 00:00:00 2001 From: kiraware <117554978+kiraware@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:23:19 +0800 Subject: [PATCH] Release 1.0.0 (#5) * update tests update tests so that it reflect the new api change using facade design pattern. * update docs this update the reference and tutorial to reflect new facade api design pattern. * move enums outside weather_forecast folder move these enums in order to implement facade design pattern. * use one file for exception there are only two exception, so just place it in one file. * use only one file for types * move schema into one folder move all schemas into one folder and create a new schema called `Shakemap` for storing information about shakemap. * move all parser into separate file in one folder * create api folder this api folder contains all api wrapper needed by pybmkg. * change version to 1.0.0 * small typo in error msg * add test_parsers these tests mainly testing the exception that may raised by parser. * update tests some tests is not work as expected due to __getitem__ mock. Hopefully now it fixed. --- docs/reference/api.md | 6 +- docs/reference/enum.md | 2 +- docs/reference/exception.md | 3 +- docs/reference/schema.md | 4 +- docs/tutorials.md | 45 +- poetry.lock | 300 ++++---- pyproject.toml | 2 +- src/bmkg/__init__.py | 8 +- src/bmkg/api/__init__.py | 3 + src/bmkg/api/api.py | 14 + src/bmkg/api/bmkg.py | 39 + src/bmkg/api/earthquake.py | 89 +++ src/bmkg/api/shakemap.py | 40 ++ .../weather_forecast.py | 17 +- src/bmkg/bmkg.py | 27 - src/bmkg/common/exception.py | 9 - src/bmkg/common/schemas/__init__.py | 6 - src/bmkg/earthquake/__init__.py | 3 - src/bmkg/earthquake/earthquake.py | 106 --- src/bmkg/earthquake/parser.py | 202 ------ src/bmkg/earthquake/schemas/__init__.py | 11 - .../{weather_forecast => }/enums/__init__.py | 2 +- .../{weather_forecast => }/enums/cardinal.py | 0 .../{weather_forecast => }/enums/province.py | 0 src/bmkg/{weather_forecast => }/enums/sexa.py | 0 src/bmkg/{weather_forecast => }/enums/type.py | 0 .../{weather_forecast => }/enums/weather.py | 0 src/bmkg/exception.py | 20 + src/bmkg/parsers/__init__.py | 33 + src/bmkg/parsers/parse_area_element.py | 112 +++ src/bmkg/parsers/parse_data_element.py | 43 ++ src/bmkg/parsers/parse_datetime_element.py | 64 ++ src/bmkg/parsers/parse_earthquake_data.py | 54 ++ .../parsers/parse_felt_earthquake_data.py | 51 ++ src/bmkg/parsers/parse_forecast_element.py | 35 + src/bmkg/parsers/parse_humidity_element.py | 56 ++ src/bmkg/parsers/parse_issue_element.py | 50 ++ .../parsers/parse_latest_earthquake_data.py | 51 ++ .../parsers/parse_strong_earthquake_data.py | 51 ++ src/bmkg/parsers/parse_temperature_element.py | 63 ++ src/bmkg/parsers/parse_weather_element.py | 54 ++ .../parsers/parse_weather_forecast_data.py | 136 ++++ .../parsers/parse_wind_direction_element.py | 71 ++ src/bmkg/parsers/parse_wind_speed_element.py | 77 ++ .../schemas/__init__.py | 12 + .../{weather_forecast => }/schemas/area.py | 2 +- src/bmkg/{common => }/schemas/coordinate.py | 0 .../{weather_forecast => }/schemas/data.py | 0 .../{earthquake => }/schemas/earthquake.py | 2 +- .../schemas/felt_earthquake.py | 0 .../schemas/forecast.py | 0 .../schemas/humidity.py | 0 .../schemas/latest_earthquake.py | 4 +- .../{weather_forecast => }/schemas/name.py | 0 src/bmkg/schemas/shakemap.py | 15 + .../schemas/strong_earthquake.py | 0 .../schemas/temperature.py | 0 .../{weather_forecast => }/schemas/weather.py | 0 .../schemas/weather_forecast.py | 0 .../schemas/wind_direction.py | 0 .../schemas/wind_speed.py | 0 src/bmkg/{earthquake => }/types.py | 44 +- src/bmkg/weather_forecast/__init__.py | 3 - src/bmkg/weather_forecast/exception.py | 11 - src/bmkg/weather_forecast/parser.py | 671 ------------------ src/bmkg/weather_forecast/types.py | 43 -- {src/bmkg/common => tests}/__init__.py | 0 tests/test_earthquake/test_integration.py | 22 - tests/test_earthquake_integration.py | 22 + .../{test_weather_forecast => }/test_enum.py | 2 +- .../__init__.py | 0 tests/test_parsers/test_parse_area_element.py | 62 ++ tests/test_parsers/test_parse_data_element.py | 21 + .../test_parse_datetime_element.py | 19 + .../test_parse_forecast_element.py | 16 + .../test_parse_humidity_element.py | 35 + .../test_parsers/test_parse_issue_element.py | 28 + .../test_parse_temperature_element.py | 53 ++ .../test_parse_weather_element.py | 35 + .../test_parse_wind_direction_element.py | 54 ++ .../test_parse_wind_speed_element.py | 55 ++ tests/test_weather_forecast/__init__.py | 0 ...y => test_weather_forecast_integration.py} | 10 +- 83 files changed, 1884 insertions(+), 1316 deletions(-) create mode 100644 src/bmkg/api/__init__.py create mode 100644 src/bmkg/api/api.py create mode 100644 src/bmkg/api/bmkg.py create mode 100644 src/bmkg/api/earthquake.py create mode 100644 src/bmkg/api/shakemap.py rename src/bmkg/{weather_forecast => api}/weather_forecast.py (68%) delete mode 100644 src/bmkg/bmkg.py delete mode 100644 src/bmkg/common/exception.py delete mode 100644 src/bmkg/common/schemas/__init__.py delete mode 100644 src/bmkg/earthquake/__init__.py delete mode 100644 src/bmkg/earthquake/earthquake.py delete mode 100644 src/bmkg/earthquake/parser.py delete mode 100644 src/bmkg/earthquake/schemas/__init__.py rename src/bmkg/{weather_forecast => }/enums/__init__.py (78%) rename src/bmkg/{weather_forecast => }/enums/cardinal.py (100%) rename src/bmkg/{weather_forecast => }/enums/province.py (100%) rename src/bmkg/{weather_forecast => }/enums/sexa.py (100%) rename src/bmkg/{weather_forecast => }/enums/type.py (100%) rename src/bmkg/{weather_forecast => }/enums/weather.py (100%) create mode 100644 src/bmkg/exception.py create mode 100644 src/bmkg/parsers/__init__.py create mode 100644 src/bmkg/parsers/parse_area_element.py create mode 100644 src/bmkg/parsers/parse_data_element.py create mode 100644 src/bmkg/parsers/parse_datetime_element.py create mode 100644 src/bmkg/parsers/parse_earthquake_data.py create mode 100644 src/bmkg/parsers/parse_felt_earthquake_data.py create mode 100644 src/bmkg/parsers/parse_forecast_element.py create mode 100644 src/bmkg/parsers/parse_humidity_element.py create mode 100644 src/bmkg/parsers/parse_issue_element.py create mode 100644 src/bmkg/parsers/parse_latest_earthquake_data.py create mode 100644 src/bmkg/parsers/parse_strong_earthquake_data.py create mode 100644 src/bmkg/parsers/parse_temperature_element.py create mode 100644 src/bmkg/parsers/parse_weather_element.py create mode 100644 src/bmkg/parsers/parse_weather_forecast_data.py create mode 100644 src/bmkg/parsers/parse_wind_direction_element.py create mode 100644 src/bmkg/parsers/parse_wind_speed_element.py rename src/bmkg/{weather_forecast => }/schemas/__init__.py (57%) rename src/bmkg/{weather_forecast => }/schemas/area.py (94%) rename src/bmkg/{common => }/schemas/coordinate.py (100%) rename src/bmkg/{weather_forecast => }/schemas/data.py (100%) rename src/bmkg/{earthquake => }/schemas/earthquake.py (95%) rename src/bmkg/{earthquake => }/schemas/felt_earthquake.py (100%) rename src/bmkg/{weather_forecast => }/schemas/forecast.py (100%) rename src/bmkg/{weather_forecast => }/schemas/humidity.py (100%) rename src/bmkg/{earthquake => }/schemas/latest_earthquake.py (85%) rename src/bmkg/{weather_forecast => }/schemas/name.py (100%) create mode 100644 src/bmkg/schemas/shakemap.py rename src/bmkg/{earthquake => }/schemas/strong_earthquake.py (100%) rename src/bmkg/{weather_forecast => }/schemas/temperature.py (100%) rename src/bmkg/{weather_forecast => }/schemas/weather.py (100%) rename src/bmkg/{weather_forecast => }/schemas/weather_forecast.py (100%) rename src/bmkg/{weather_forecast => }/schemas/wind_direction.py (100%) rename src/bmkg/{weather_forecast => }/schemas/wind_speed.py (100%) rename src/bmkg/{earthquake => }/types.py (52%) delete mode 100644 src/bmkg/weather_forecast/__init__.py delete mode 100644 src/bmkg/weather_forecast/exception.py delete mode 100644 src/bmkg/weather_forecast/parser.py delete mode 100644 src/bmkg/weather_forecast/types.py rename {src/bmkg/common => tests}/__init__.py (100%) delete mode 100644 tests/test_earthquake/test_integration.py create mode 100644 tests/test_earthquake_integration.py rename tests/{test_weather_forecast => }/test_enum.py (98%) rename tests/{test_earthquake => test_parsers}/__init__.py (100%) create mode 100644 tests/test_parsers/test_parse_area_element.py create mode 100644 tests/test_parsers/test_parse_data_element.py create mode 100644 tests/test_parsers/test_parse_datetime_element.py create mode 100644 tests/test_parsers/test_parse_forecast_element.py create mode 100644 tests/test_parsers/test_parse_humidity_element.py create mode 100644 tests/test_parsers/test_parse_issue_element.py create mode 100644 tests/test_parsers/test_parse_temperature_element.py create mode 100644 tests/test_parsers/test_parse_weather_element.py create mode 100644 tests/test_parsers/test_parse_wind_direction_element.py create mode 100644 tests/test_parsers/test_parse_wind_speed_element.py delete mode 100644 tests/test_weather_forecast/__init__.py rename tests/{test_weather_forecast/test_integration.py => test_weather_forecast_integration.py} (55%) diff --git a/docs/reference/api.md b/docs/reference/api.md index 5b0b62c..43c72af 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,2 +1,4 @@ -::: bmkg.earthquake -::: bmkg.weather_forecast +::: bmkg.BMKG +::: bmkg.api.earthquake.Earthquake +::: bmkg.api.weather_forecast.WeatherForecast +::: bmkg.api.shakemap.Shakemap diff --git a/docs/reference/enum.md b/docs/reference/enum.md index b44622b..33c3af8 100644 --- a/docs/reference/enum.md +++ b/docs/reference/enum.md @@ -1 +1 @@ -::: bmkg.weather_forecast.enums +::: bmkg.enums diff --git a/docs/reference/exception.md b/docs/reference/exception.md index 3286b11..b8ed9b8 100644 --- a/docs/reference/exception.md +++ b/docs/reference/exception.md @@ -1,2 +1 @@ -::: bmkg.common.exception -::: bmkg.weather_forecast.exception +::: bmkg.exception diff --git a/docs/reference/schema.md b/docs/reference/schema.md index 974a3ff..3a64a1d 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -1,3 +1 @@ -::: bmkg.common.schemas -::: bmkg.earthquake.schemas -::: bmkg.weather_forecast.schemas +::: bmkg.schemas diff --git a/docs/tutorials.md b/docs/tutorials.md index 9f78982..c2a345c 100644 --- a/docs/tutorials.md +++ b/docs/tutorials.md @@ -32,12 +32,12 @@ Code example: import asyncio from dataclasses import fields -from bmkg.earthquake import Earthquake +from bmkg import BMKG async def main(): - async with Earthquake() as earthquake: - latest_earthquake = await earthquake.get_latest_earthquake() + async with BMKG() as bmkg: + latest_earthquake = await bmkg.earthquake.get_latest_earthquake() print(latest_earthquake.earthquake.datetime) print(latest_earthquake.earthquake.coordinate) @@ -67,7 +67,7 @@ Coordinate(latitude=-3.63, longitude=140.46) 46 km BaratDaya KEEROM-PAPUA Tidak berpotensi tsunami - -20240118013237.mmi.jpg +Shakemap(file_name='20240118013237.mmi.jpg') ``` ### get_latest_earthquake_shakemap @@ -82,19 +82,18 @@ Code example: ```python import asyncio -from dataclasses import fields -from bmkg.earthquake import Earthquake +from bmkg import BMKG async def main(): - async with Earthquake() as earthquake: - latest_earthquake = await earthquake.get_latest_earthquake() + async with BMKG() as bmkg: + latest_earthquake = await bmkg.earthquake.get_latest_earthquake() + shakemap = latest_earthquake.shakemap + shakemap_content = await shakemap.get_content() - shakemap = await earthquake.get_latest_earthquake_shakemap( - latest_earthquake.shakemap - ) - print(shakemap) + print(shakemap.file_name) + print(shakemap_content) asyncio.run(main()) ``` @@ -102,6 +101,7 @@ asyncio.run(main()) Example output: ```console +20240203152510.mmi.jpg b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00...' ``` @@ -120,12 +120,12 @@ Code example: import asyncio from dataclasses import fields -from bmkg.earthquake import Earthquake +from bmkg import BMKG async def main(): - async with Earthquake() as earthquake: - strong_earthquakes = await earthquake.get_strong_earthquake() + async with BMKG() as bmkg: + strong_earthquakes = await bmkg.earthquake.get_strong_earthquake() for strong_earthquake in strong_earthquakes: print(strong_earthquake.earthquake.datetime) @@ -170,12 +170,12 @@ Code example: import asyncio from dataclasses import fields -from bmkg.earthquake import Earthquake +from bmkg import BMKG async def main(): - async with Earthquake() as earthquake: - felt_earthquakes = await earthquake.get_felt_earthquake() + async with BMKG() as bmkg: + felt_earthquakes = await bmkg.earthquake.get_felt_earthquake() for felt_earthquake in felt_earthquakes: print(felt_earthquake.earthquake.datetime) @@ -228,15 +228,14 @@ Code example: ```python import asyncio -from dataclasses import fields -from bmkg.weather_forecast import WeatherForecast -from bmkg.weather_forecast.enums import Province, Type +from bmkg import BMKG +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) + async with BMKG() as bmkg: + weather_forecast_data = await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) print(weather_forecast_data.data) print(weather_forecast_data.forecast) diff --git a/poetry.lock b/poetry.lock index ea078ee..05c4675 100644 --- a/poetry.lock +++ b/poetry.lock @@ -167,13 +167,13 @@ yaml = ["PyYAML"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -572,71 +572,71 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -708,13 +708,13 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "9.5.6" +version = "9.5.7" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.6-py3-none-any.whl", hash = "sha256:e115b90fccf5cd7f5d15b0c2f8e6246b21041628b8f590630e7fca66ed7fcf6c"}, - {file = "mkdocs_material-9.5.6.tar.gz", hash = "sha256:5b24df36d8ac6cecd611241ce6f6423ccde3e1ad89f8360c3f76d5565fc2d82a"}, + {file = "mkdocs_material-9.5.7-py3-none-any.whl", hash = "sha256:0be8ce8bcfebb52bae9b00cf9b851df45b8a92d629afcfd7f2c09b2dfa155ea3"}, + {file = "mkdocs_material-9.5.7.tar.gz", hash = "sha256:16110292575d88a338d2961f3cb665cf12943ff8829e551a9b364f24019e46af"}, ] [package.dependencies] @@ -790,85 +790,101 @@ mkdocstrings = ">=0.20" [[package]] name = "multidict" -version = "6.0.4" +version = "6.0.5" description = "multidict implementation" optional = false python-versions = ">=3.7" files = [ - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, - {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, - {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, - {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, - {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, - {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, - {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, - {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, - {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, - {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, - {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, - {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, - {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, - {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, - {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, - {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, - {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, - {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, - {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, - {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, - {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, - {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, - {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, - {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, - {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, - {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, - {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 1018d07..8147e23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "PyBMKG" -version = "0.2.0" +version = "1.0.0" description = "Python BMKG API Wrapper" authors = ["Kira "] packages = [ diff --git a/src/bmkg/__init__.py b/src/bmkg/__init__.py index 112111a..6724f8b 100644 --- a/src/bmkg/__init__.py +++ b/src/bmkg/__init__.py @@ -1,7 +1,3 @@ -from .earthquake import Earthquake -from .weather_forecast import WeatherForecast +from .api import BMKG -__all__ = [ - "Earthquake", - "WeatherForecast", -] +__all__ = ["BMKG"] diff --git a/src/bmkg/api/__init__.py b/src/bmkg/api/__init__.py new file mode 100644 index 0000000..d754f0a --- /dev/null +++ b/src/bmkg/api/__init__.py @@ -0,0 +1,3 @@ +from .bmkg import BMKG + +__all__ = ["BMKG"] diff --git a/src/bmkg/api/api.py b/src/bmkg/api/api.py new file mode 100644 index 0000000..eb37f9c --- /dev/null +++ b/src/bmkg/api/api.py @@ -0,0 +1,14 @@ +from aiohttp import ClientSession + +__all__ = ["API"] + + +class API: + """ + Base API class. + """ + + def __init__(self, session: ClientSession | None = None) -> None: + self._session = ( + session if session is not None else ClientSession("https://data.bmkg.go.id") + ) diff --git a/src/bmkg/api/bmkg.py b/src/bmkg/api/bmkg.py new file mode 100644 index 0000000..a6afc21 --- /dev/null +++ b/src/bmkg/api/bmkg.py @@ -0,0 +1,39 @@ +from traceback import TracebackException +from types import TracebackType +from typing import Self + +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) -> None: + API.__init__(self) + + self.earthquake = Earthquake(self._session) + self.weather_forecast = WeatherForecast(self._session) + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: Exception, + exc_val: TracebackException, + traceback: TracebackType, + ) -> None: + await self.close() + + async def close(self) -> None: + await self._session.close() diff --git a/src/bmkg/api/earthquake.py b/src/bmkg/api/earthquake.py new file mode 100644 index 0000000..9ef789a --- /dev/null +++ b/src/bmkg/api/earthquake.py @@ -0,0 +1,89 @@ +from collections.abc import Iterator + +from .. import schemas +from ..parsers import ( + parse_felt_earthquake_data, + parse_latest_earthquake_data, + parse_strong_earthquake_data, +) +from .api import API +from .shakemap import Shakemap + +__all__ = ["Earthquake"] + + +class Earthquake(API): + """ + Earthquake API Wrapper from BMKG API. + """ + + url = "/DataMKG/TEWS" + + async def get_latest_earthquake(self) -> schemas.LatestEarthquake: + """ + Request latest earthquake from earthquake API. + + Returns: + A `LatestEarthquake` schema. + + Examples: + >>> import asyncio + >>> from bmkg import BMKG + >>> async def main(): + ... async with BMKG() as bmkg: + ... latest_earthquake = await bmkg.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 + + latest_earthquake.shakemap = Shakemap( + self._session, latest_earthquake.shakemap.file_name + ) + + return latest_earthquake + + async def get_strong_earthquake(self) -> Iterator[schemas.StrongEarthquake]: + """ + Request strong earthquake that has magnitude 5.0 above from earthquake API. + + Returns: + An iterator of fifteen `StrongEarthquake` schema. + + Examples: + >>> import asyncio + >>> from bmkg import BMKG + >>> async def main(): + ... async with BMKG() as bmkg: + ... strong_earthquake = await bmkg.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 + + async def get_felt_earthquake(self) -> Iterator[schemas.FeltEarthquake]: + """ + Request felt earthquake from earthquake API. + + Returns: + An iterator of fifteen `FeltEarthquake` schema. + + Examples: + >>> import asyncio + >>> from bmkg import BMKG + >>> async def main(): + ... async with BMKG() as bmkg: + ... felt_earthquake = await bmkg.earthquake.get_felt_earthquake() + ... print(felt_earthquake) + >>> asyncio.run(main()) + + """ + async with self._session.get(f"{self.url}/gempadirasakan.json") as response: + return parse_felt_earthquake_data(await response.json()) # type: ignore diff --git a/src/bmkg/api/shakemap.py b/src/bmkg/api/shakemap.py new file mode 100644 index 0000000..0cb2c90 --- /dev/null +++ b/src/bmkg/api/shakemap.py @@ -0,0 +1,40 @@ +from aiohttp import ClientSession + +from .. import schemas +from .api import API + +__all__ = ["Shakemap"] + + +class Shakemap(API, schemas.Shakemap): + """ + Shakemap API Wrapper from BMKG API. + """ + + url = "/DataMKG/TEWS" + + def __init__(self, session: ClientSession, file_name: str) -> None: + API.__init__(self, session) + schemas.Shakemap.__init__(self, file_name) + + async def get_content(self) -> bytes: + """ + Get the shakemap file content. + + Returns: + A bytes of `Shakemap` image. + + Examples: + >>> 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_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/weather_forecast/weather_forecast.py b/src/bmkg/api/weather_forecast.py similarity index 68% rename from src/bmkg/weather_forecast/weather_forecast.py rename to src/bmkg/api/weather_forecast.py index 5057d22..3a26ace 100644 --- a/src/bmkg/weather_forecast/weather_forecast.py +++ b/src/bmkg/api/weather_forecast.py @@ -1,12 +1,12 @@ -from ..bmkg import BMKG -from .enums import Province -from .parser import parse_weather_forecast_data -from .schemas import WeatherForecast as WeatherForecastData +from ..enums import Province +from ..parsers import parse_weather_forecast_data +from ..schemas import WeatherForecast as WeatherForecastData +from .api import API __all__ = ["WeatherForecast"] -class WeatherForecast(BMKG): +class WeatherForecast(API): """ Weather Forecast API Wrapper from BMKG API. """ @@ -25,10 +25,11 @@ async def get_weather_forecast(self, province: Province) -> WeatherForecastData: Examples: >>> import asyncio + >>> from bmkg import BMKG >>> async def main(): - ... async with WeatherForecast() as weather_forecast: - ... weather_forecast_data = await weather_forecast.get_weather_forecast( - ... Province.ACEH + ... async with BMKG() as bmkg: + ... weather_forecast_data = ( + ... await bmkg.weather_forecast.get_weather_forecast(Province.ACEH) ... ) ... print(weather_forecast_data) >>> asyncio.run(main()) diff --git a/src/bmkg/bmkg.py b/src/bmkg/bmkg.py deleted file mode 100644 index fdc5052..0000000 --- a/src/bmkg/bmkg.py +++ /dev/null @@ -1,27 +0,0 @@ -from traceback import TracebackException -from types import TracebackType -from typing import Self - -from aiohttp import ClientSession - - -class BMKG: - """ - Base BMKG API wrapper. - """ - - async def __aenter__(self) -> Self: - self._session = ClientSession("https://data.bmkg.go.id") - - return self - - async def __aexit__( - self, - exc_type: Exception, - exc_val: TracebackException, - traceback: TracebackType, - ) -> None: - await self.close() - - async def close(self) -> None: - await self._session.close() diff --git a/src/bmkg/common/exception.py b/src/bmkg/common/exception.py deleted file mode 100644 index 5346c28..0000000 --- a/src/bmkg/common/exception.py +++ /dev/null @@ -1,9 +0,0 @@ -__all__ = ["BMKGError"] - - -class BMKGError(Exception): - """ - General BMKG API exception. - """ - - pass diff --git a/src/bmkg/common/schemas/__init__.py b/src/bmkg/common/schemas/__init__.py deleted file mode 100644 index 1319e69..0000000 --- a/src/bmkg/common/schemas/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -This module contain common schema used by bmkg api wrapper. -""" -from .coordinate import Coordinate - -__all__ = ["Coordinate"] diff --git a/src/bmkg/earthquake/__init__.py b/src/bmkg/earthquake/__init__.py deleted file mode 100644 index 1058a44..0000000 --- a/src/bmkg/earthquake/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .earthquake import Earthquake - -__all__ = ["Earthquake"] diff --git a/src/bmkg/earthquake/earthquake.py b/src/bmkg/earthquake/earthquake.py deleted file mode 100644 index e258733..0000000 --- a/src/bmkg/earthquake/earthquake.py +++ /dev/null @@ -1,106 +0,0 @@ -from collections.abc import Iterator - -from ..bmkg import BMKG -from .parser import ( - parse_felt_earthquake_data, - parse_latest_earthquake_data, - parse_strong_earthquake_data, -) -from .schemas import ( - FeltEarthquake, - LatestEarthquake, - StrongEarthquake, -) -from .types import Shakemap - -__all__ = ["Earthquake"] - - -class Earthquake(BMKG): - """ - Earthquake API Wrapper from BMKG API. - """ - - url = "/DataMKG/TEWS" - - async def get_latest_earthquake(self) -> LatestEarthquake: - """ - Request latest earthquake from earthquake API. - - Returns: - A `LatestEarthquake` schema. - - Examples: - >>> import asyncio - >>> async def main(): - ... async with Earthquake() as earthquake: - ... latest_earthquake = await earthquake.get_latest_earthquake() - ... print(latest_earthquake) - >>> asyncio.run(main()) - LatestEarthquake(earthquake=Earthquake(datetime=datetime.datetime(...) - """ - async with self._session.get(f"{self.url}/autogempa.json") as response: - return parse_latest_earthquake_data(await response.json()) - - async def get_latest_earthquake_shakemap(self, shakemap: Shakemap) -> bytes: - """ - Request latest earthquake shakemap from earthquake API. - - Args: - shakemap: A `Shakemap` file name. - - Returns: - A bytes of `Shakemap` image. - - Examples: - >>> import asyncio - >>> async def main(): - ... async with Earthquake() as earthquake: - ... latest_earthquake = await earthquake.get_latest_earthquake() - ... shakemap = await earthquake.get_latest_earthquake_shakemap( - ... latest_earthquake.shakemap - ... ) - ... print(shakemap) - >>> asyncio.run(main()) - b'...' - """ - async with self._session.get(f"{self.url}/{shakemap}") as response: - return await response.read() - - async def get_strong_earthquake(self) -> Iterator[StrongEarthquake]: - """ - Request strong earthquake that has magnitude 5.0 above from earthquake API. - - Returns: - An iterator of fifteen `StrongEarthquake` schema. - - Examples: - >>> import asyncio - >>> async def main(): - ... async with Earthquake() as earthquake: - ... strong_earthquake = await earthquake.get_strong_earthquake() - ... print(strong_earthquake) - >>> asyncio.run(main()) - - """ - async with self._session.get(f"{self.url}/gempaterkini.json") as response: - return parse_strong_earthquake_data(await response.json()) - - async def get_felt_earthquake(self) -> Iterator[FeltEarthquake]: - """ - Request felt earthquake from earthquake API. - - Returns: - An iterator of fifteen `FeltEarthquake` schema. - - Examples: - >>> import asyncio - >>> async def main(): - ... async with Earthquake() as earthquake: - ... felt_earthquake = await earthquake.get_felt_earthquake() - ... print(felt_earthquake) - >>> asyncio.run(main()) - - """ - async with self._session.get(f"{self.url}/gempadirasakan.json") as response: - return parse_felt_earthquake_data(await response.json()) diff --git a/src/bmkg/earthquake/parser.py b/src/bmkg/earthquake/parser.py deleted file mode 100644 index 504e656..0000000 --- a/src/bmkg/earthquake/parser.py +++ /dev/null @@ -1,202 +0,0 @@ -from collections.abc import Iterator -from datetime import datetime - -from ..common.schemas import Coordinate -from .schemas import ( - Earthquake, - FeltEarthquake, - LatestEarthquake, - StrongEarthquake, -) -from .types import ( - EarthquakeData, - InfoFeltEarthquakeData, - InfoLatestEarthquakeData, - InfoStrongEarthquakeData, -) - -__all__ = [ - "parse_earthquake_data", - "parse_latest_earthquake_data", - "parse_strong_earthquake_data", - "parse_felt_earthquake_data", -] - - -def parse_earthquake_data(earthquake_data: EarthquakeData) -> Earthquake: - """ - Parse `EarthquakeData` JSON. - - Args: - earthquake_data: A dict representing JSON of earthquake. - - Returns: - An `Earthquake` schema. - - Examples: - >>> earthquake_data = EarthquakeData( - ... { - ... "Tanggal": "14 Jan 2024", - ... "Jam": "03:13:37 WIB", - ... "DateTime": "2024-01-13T20:13:37+00:00", - ... "Coordinates": "-6.75,106.55", - ... "Lintang": "6.75 LS", - ... "Bujur": "106.55 BT", - ... "Magnitude": "3.5", - ... "Kedalaman": "5 km", - ... "Wilayah": "Pusat gempa berada di darat 26 km Utara Kab Sukabumi", - ... } - ... ) - >>> parse_earthquake_data(earthquake_data) - Earthquake(datetime=datetime.datetime(2024, 1, 13, 20, 13, 37, tzinfo=datetim... - """ - - dt = earthquake_data["DateTime"] - coordinate = earthquake_data["Coordinates"].split(",") - latitude = coordinate[0] - longitude = coordinate[1] - magnitude = earthquake_data["Magnitude"] - depth = earthquake_data["Kedalaman"].split()[0] - region = earthquake_data["Wilayah"] - - return Earthquake( - datetime.fromisoformat(dt), - Coordinate(float(latitude), float(longitude)), - float(magnitude), - float(depth), - region, - ) - - -def parse_latest_earthquake_data( - info_latest_earthquake_data: InfoLatestEarthquakeData, -) -> LatestEarthquake: - """ - Parse `InfoLatestEarthquakeData` JSON. - - Args: - info_latest_earthquake_data: A dict representing JSON of info latest earthquake. - - Returns: - A `LatestEarthquake` schema. - - Examples: - >>> info_latest_earthquake_data = InfoLatestEarthquakeData( - ... { - ... "Infogempa": { - ... "gempa": { - ... "Tanggal": "15 Jan 2024", - ... "Jam": "16:07:08 WIB", - ... "DateTime": "2024-01-15T09:07:08+00:00", - ... "Coordinates": "-3.98,122.45", - ... "Lintang": "3.98 LS", - ... "Bujur": "122.45 BT", - ... "Magnitude": "2.6", - ... "Kedalaman": "9 km", - ... "Wilayah": "Pusat gempa berada di darat 2,5 km BaratDaya P", - ... "Potensi": "Gempa ini dirasakan untuk diteruskan pada masy", - ... "Dirasakan": "II Kendari", - ... "Shakemap": "20240115160708.mmi.jpg", - ... } - ... } - ... } - ... ) - >>> parse_latest_earthquake_data(info_latest_earthquake_data) - LatestEarthquake(earthquake=Earthquake(datetime=datetime.datetime(2024, 1, 15... - """ - - latest_earthquake_data = info_latest_earthquake_data["Infogempa"]["gempa"] - earthquake = parse_earthquake_data(latest_earthquake_data) - potency = latest_earthquake_data["Potensi"] - felt = latest_earthquake_data["Dirasakan"] - shakemap = latest_earthquake_data["Shakemap"] - - return LatestEarthquake(earthquake, potency, felt, shakemap) - - -def parse_strong_earthquake_data( - info_strong_earthquake_data: InfoStrongEarthquakeData, -) -> Iterator[StrongEarthquake]: - """ - Parse `InfoStrongEarthquakeData` JSON. - - Args: - info_strong_earthquake_data: A dict representing JSON of info strong earthquake. - - Yields: - A `StrongEarthquake` schema. - - Examples: - >>> info_strong_earthquake_data = InfoLatestEarthquakeData( - ... { - ... "Infogempa": { - ... "gempa": [ - ... { - ... "Tanggal": "09 Jan 2024", - ... "Jam": "12:25:24 WIB", - ... "DateTime": "2024-01-09T05:25:24+00:00", - ... "Coordinates": "-4.46,133.91", - ... "Lintang": "4.46 LS", - ... "Bujur": "133.91 BT", - ... "Magnitude": "5.3", - ... "Kedalaman": "10 km", - ... "Wilayah": "130 km BaratDaya KAIMANA-PAPUABRT", - ... "Potensi": "Tidak berpotensi tsunami", - ... } - ... ] - ... } - ... } - ... ) - >>> parse_strong_earthquake_data(info_strong_earthquake_data) - - """ - - for strong_earthquake_data in info_strong_earthquake_data["Infogempa"]["gempa"]: - earthquake = parse_earthquake_data(strong_earthquake_data) - potency = strong_earthquake_data["Potensi"] - - yield StrongEarthquake(earthquake, potency) - - -def parse_felt_earthquake_data( - info_felt_earthquake_data: InfoFeltEarthquakeData, -) -> Iterator[FeltEarthquake]: - """ - Parse `InfoFeltEarthquakeData` JSON. - - Args: - info_felt_earthquake_data: A dict representing JSON of info felt earthquake. - - Yields: - A `FeltEarthquake` schema. - - Examples: - >>> info_felt_earthquake_data = InfoFeltEarthquakeData( - ... { - ... "Infogempa": { - ... "gempa": [ - ... { - ... "Tanggal": "15 Jan 2024", - ... "Jam": "16:07:08 WIB", - ... "DateTime": "2024-01-15T09:07:08+00:00", - ... "Coordinates": "-3.98,122.45", - ... "Lintang": "3.98 LS", - ... "Bujur": "122.45 BT", - ... "Magnitude": "2.6", - ... "Kedalaman": "9 km", - ... "Wilayah": "Pusat gempa berada di darat 2,5 km BaratDa", - ... "Dirasakan": "II Kendari", - ... } - ... ] - ... } - ... } - ... ) - >>> parse_felt_earthquake_data(info_felt_earthquake_data) - - """ - - for felt_earthquake_data in info_felt_earthquake_data["Infogempa"]["gempa"]: - earthquake = parse_earthquake_data(felt_earthquake_data) - felt = felt_earthquake_data["Dirasakan"] - - yield FeltEarthquake(earthquake, felt) diff --git a/src/bmkg/earthquake/schemas/__init__.py b/src/bmkg/earthquake/schemas/__init__.py deleted file mode 100644 index 4b35505..0000000 --- a/src/bmkg/earthquake/schemas/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .earthquake import Earthquake -from .felt_earthquake import FeltEarthquake -from .latest_earthquake import LatestEarthquake -from .strong_earthquake import StrongEarthquake - -__all__ = [ - "Earthquake", - "FeltEarthquake", - "LatestEarthquake", - "StrongEarthquake", -] diff --git a/src/bmkg/weather_forecast/enums/__init__.py b/src/bmkg/enums/__init__.py similarity index 78% rename from src/bmkg/weather_forecast/enums/__init__.py rename to src/bmkg/enums/__init__.py index 5fbb7dd..54371a4 100644 --- a/src/bmkg/weather_forecast/enums/__init__.py +++ b/src/bmkg/enums/__init__.py @@ -1,5 +1,5 @@ """ -Module to store common enum classes used by weather forecast. +Module to store common enum classes used by bmkg. """ from .cardinal import Cardinal from .province import Province diff --git a/src/bmkg/weather_forecast/enums/cardinal.py b/src/bmkg/enums/cardinal.py similarity index 100% rename from src/bmkg/weather_forecast/enums/cardinal.py rename to src/bmkg/enums/cardinal.py diff --git a/src/bmkg/weather_forecast/enums/province.py b/src/bmkg/enums/province.py similarity index 100% rename from src/bmkg/weather_forecast/enums/province.py rename to src/bmkg/enums/province.py diff --git a/src/bmkg/weather_forecast/enums/sexa.py b/src/bmkg/enums/sexa.py similarity index 100% rename from src/bmkg/weather_forecast/enums/sexa.py rename to src/bmkg/enums/sexa.py diff --git a/src/bmkg/weather_forecast/enums/type.py b/src/bmkg/enums/type.py similarity index 100% rename from src/bmkg/weather_forecast/enums/type.py rename to src/bmkg/enums/type.py diff --git a/src/bmkg/weather_forecast/enums/weather.py b/src/bmkg/enums/weather.py similarity index 100% rename from src/bmkg/weather_forecast/enums/weather.py rename to src/bmkg/enums/weather.py diff --git a/src/bmkg/exception.py b/src/bmkg/exception.py new file mode 100644 index 0000000..400dbf7 --- /dev/null +++ b/src/bmkg/exception.py @@ -0,0 +1,20 @@ +__all__ = [ + "BMKGError", + "WeatherForecastParseError", +] + + +class BMKGError(Exception): + """ + General BMKG API exception. + """ + + pass + + +class WeatherForecastParseError(BMKGError): + """ + Weather forecast parse exception. + """ + + pass diff --git a/src/bmkg/parsers/__init__.py b/src/bmkg/parsers/__init__.py new file mode 100644 index 0000000..597ff36 --- /dev/null +++ b/src/bmkg/parsers/__init__.py @@ -0,0 +1,33 @@ +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_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_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_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_forecast_data", +] diff --git a/src/bmkg/parsers/parse_area_element.py b/src/bmkg/parsers/parse_area_element.py new file mode 100644 index 0000000..d2efd55 --- /dev/null +++ b/src/bmkg/parsers/parse_area_element.py @@ -0,0 +1,112 @@ +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..enums import Type +from ..exception 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 new file mode 100644 index 0000000..3255178 --- /dev/null +++ b/src/bmkg/parsers/parse_data_element.py @@ -0,0 +1,43 @@ +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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 new file mode 100644 index 0000000..b624560 --- /dev/null +++ b/src/bmkg/parsers/parse_datetime_element.py @@ -0,0 +1,64 @@ +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 ..exception 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_earthquake_data.py b/src/bmkg/parsers/parse_earthquake_data.py new file mode 100644 index 0000000..de80a9a --- /dev/null +++ b/src/bmkg/parsers/parse_earthquake_data.py @@ -0,0 +1,54 @@ +from datetime import datetime + +from ..schemas import ( + Coordinate, + Earthquake, +) +from ..types import EarthquakeData + +__all__ = ["parse_earthquake_data"] + + +def parse_earthquake_data(earthquake_data: EarthquakeData) -> Earthquake: + """ + Parse `EarthquakeData` JSON. + + Args: + earthquake_data: A dict representing JSON of earthquake. + + Returns: + An `Earthquake` schema. + + Examples: + >>> earthquake_data = EarthquakeData( + ... { + ... "Tanggal": "14 Jan 2024", + ... "Jam": "03:13:37 WIB", + ... "DateTime": "2024-01-13T20:13:37+00:00", + ... "Coordinates": "-6.75,106.55", + ... "Lintang": "6.75 LS", + ... "Bujur": "106.55 BT", + ... "Magnitude": "3.5", + ... "Kedalaman": "5 km", + ... "Wilayah": "Pusat gempa berada di darat 26 km Utara Kab Sukabumi", + ... } + ... ) + >>> parse_earthquake_data(earthquake_data) + Earthquake(datetime=datetime.datetime(2024, 1, 13, 20, 13, 37, tzinfo=datetim... + """ + + dt = earthquake_data["DateTime"] + coordinate = earthquake_data["Coordinates"].split(",") + latitude = coordinate[0] + longitude = coordinate[1] + magnitude = earthquake_data["Magnitude"] + depth = earthquake_data["Kedalaman"].split()[0] + region = earthquake_data["Wilayah"] + + return Earthquake( + datetime.fromisoformat(dt), + Coordinate(float(latitude), float(longitude)), + float(magnitude), + float(depth), + region, + ) diff --git a/src/bmkg/parsers/parse_felt_earthquake_data.py b/src/bmkg/parsers/parse_felt_earthquake_data.py new file mode 100644 index 0000000..814ca70 --- /dev/null +++ b/src/bmkg/parsers/parse_felt_earthquake_data.py @@ -0,0 +1,51 @@ +from collections.abc import Iterator + +from ..schemas import FeltEarthquake +from ..types import InfoFeltEarthquakeData +from .parse_earthquake_data import parse_earthquake_data + +__all__ = ["parse_felt_earthquake_data"] + + +def parse_felt_earthquake_data( + info_felt_earthquake_data: InfoFeltEarthquakeData, +) -> Iterator[FeltEarthquake]: + """ + Parse `InfoFeltEarthquakeData` JSON. + + Args: + info_felt_earthquake_data: A dict representing JSON of info felt earthquake. + + Yields: + A `FeltEarthquake` schema. + + Examples: + >>> info_felt_earthquake_data = InfoFeltEarthquakeData( + ... { + ... "Infogempa": { + ... "gempa": [ + ... { + ... "Tanggal": "15 Jan 2024", + ... "Jam": "16:07:08 WIB", + ... "DateTime": "2024-01-15T09:07:08+00:00", + ... "Coordinates": "-3.98,122.45", + ... "Lintang": "3.98 LS", + ... "Bujur": "122.45 BT", + ... "Magnitude": "2.6", + ... "Kedalaman": "9 km", + ... "Wilayah": "Pusat gempa berada di darat 2,5 km BaratDa", + ... "Dirasakan": "II Kendari", + ... } + ... ] + ... } + ... } + ... ) + >>> parse_felt_earthquake_data(info_felt_earthquake_data) + + """ + + for felt_earthquake_data in info_felt_earthquake_data["Infogempa"]["gempa"]: + earthquake = parse_earthquake_data(felt_earthquake_data) + felt = felt_earthquake_data["Dirasakan"] + + yield FeltEarthquake(earthquake, felt) diff --git a/src/bmkg/parsers/parse_forecast_element.py b/src/bmkg/parsers/parse_forecast_element.py new file mode 100644 index 0000000..576b770 --- /dev/null +++ b/src/bmkg/parsers/parse_forecast_element.py @@ -0,0 +1,35 @@ +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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 new file mode 100644 index 0000000..a1f607c --- /dev/null +++ b/src/bmkg/parsers/parse_humidity_element.py @@ -0,0 +1,56 @@ +from collections.abc import Iterator + +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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 new file mode 100644 index 0000000..4c1a362 --- /dev/null +++ b/src/bmkg/parsers/parse_issue_element.py @@ -0,0 +1,50 @@ +from datetime import datetime + +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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) + """ + timestamp_element = element.find("timestamp") + if timestamp_element is None: + raise WeatherForecastParseError("timestamp tag in issue tag not found") + + timestamp = timestamp_element.text + if timestamp is None: + raise WeatherForecastParseError("timestamp tag in issue tag has no text") + + return datetime.strptime(timestamp, "%Y%m%d%H%M%S") diff --git a/src/bmkg/parsers/parse_latest_earthquake_data.py b/src/bmkg/parsers/parse_latest_earthquake_data.py new file mode 100644 index 0000000..f5af4ab --- /dev/null +++ b/src/bmkg/parsers/parse_latest_earthquake_data.py @@ -0,0 +1,51 @@ +from ..schemas import LatestEarthquake, Shakemap +from ..types import InfoLatestEarthquakeData +from .parse_earthquake_data import parse_earthquake_data + +__all__ = ["parse_latest_earthquake_data"] + + +def parse_latest_earthquake_data( + info_latest_earthquake_data: InfoLatestEarthquakeData, +) -> LatestEarthquake: + """ + Parse `InfoLatestEarthquakeData` JSON. + + Args: + info_latest_earthquake_data: A dict representing JSON of info latest earthquake. + + Returns: + A `LatestEarthquake` schema. + + Examples: + >>> info_latest_earthquake_data = InfoLatestEarthquakeData( + ... { + ... "Infogempa": { + ... "gempa": { + ... "Tanggal": "15 Jan 2024", + ... "Jam": "16:07:08 WIB", + ... "DateTime": "2024-01-15T09:07:08+00:00", + ... "Coordinates": "-3.98,122.45", + ... "Lintang": "3.98 LS", + ... "Bujur": "122.45 BT", + ... "Magnitude": "2.6", + ... "Kedalaman": "9 km", + ... "Wilayah": "Pusat gempa berada di darat 2,5 km BaratDaya P", + ... "Potensi": "Gempa ini dirasakan untuk diteruskan pada masy", + ... "Dirasakan": "II Kendari", + ... "Shakemap": "20240115160708.mmi.jpg", + ... } + ... } + ... } + ... ) + >>> parse_latest_earthquake_data(info_latest_earthquake_data) + LatestEarthquake(earthquake=Earthquake(datetime=datetime.datetime(2024, 1, 15... + """ + + latest_earthquake_data = info_latest_earthquake_data["Infogempa"]["gempa"] + earthquake = parse_earthquake_data(latest_earthquake_data) + potency = latest_earthquake_data["Potensi"] + felt = latest_earthquake_data["Dirasakan"] + shakemap = latest_earthquake_data["Shakemap"] + + return LatestEarthquake(earthquake, potency, felt, Shakemap(shakemap)) diff --git a/src/bmkg/parsers/parse_strong_earthquake_data.py b/src/bmkg/parsers/parse_strong_earthquake_data.py new file mode 100644 index 0000000..0ce5ce7 --- /dev/null +++ b/src/bmkg/parsers/parse_strong_earthquake_data.py @@ -0,0 +1,51 @@ +from collections.abc import Iterator + +from ..schemas import StrongEarthquake +from ..types import InfoStrongEarthquakeData +from .parse_earthquake_data import parse_earthquake_data + +__all__ = ["parse_strong_earthquake_data"] + + +def parse_strong_earthquake_data( + info_strong_earthquake_data: InfoStrongEarthquakeData, +) -> Iterator[StrongEarthquake]: + """ + Parse `InfoStrongEarthquakeData` JSON. + + Args: + info_strong_earthquake_data: A dict representing JSON of info strong earthquake. + + Yields: + A `StrongEarthquake` schema. + + Examples: + >>> info_strong_earthquake_data = InfoStrongEarthquakeData( + ... { + ... "Infogempa": { + ... "gempa": [ + ... { + ... "Tanggal": "09 Jan 2024", + ... "Jam": "12:25:24 WIB", + ... "DateTime": "2024-01-09T05:25:24+00:00", + ... "Coordinates": "-4.46,133.91", + ... "Lintang": "4.46 LS", + ... "Bujur": "133.91 BT", + ... "Magnitude": "5.3", + ... "Kedalaman": "10 km", + ... "Wilayah": "130 km BaratDaya KAIMANA-PAPUABRT", + ... "Potensi": "Tidak berpotensi tsunami", + ... } + ... ] + ... } + ... } + ... ) + >>> parse_strong_earthquake_data(info_strong_earthquake_data) + + """ + + for strong_earthquake_data in info_strong_earthquake_data["Infogempa"]["gempa"]: + earthquake = parse_earthquake_data(strong_earthquake_data) + potency = strong_earthquake_data["Potensi"] + + yield StrongEarthquake(earthquake, potency) diff --git a/src/bmkg/parsers/parse_temperature_element.py b/src/bmkg/parsers/parse_temperature_element.py new file mode 100644 index 0000000..e9f39b9 --- /dev/null +++ b/src/bmkg/parsers/parse_temperature_element.py @@ -0,0 +1,63 @@ +from collections.abc import Iterator + +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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_element.py b/src/bmkg/parsers/parse_weather_element.py new file mode 100644 index 0000000..1b0eac1 --- /dev/null +++ b/src/bmkg/parsers/parse_weather_element.py @@ -0,0 +1,54 @@ +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 ..exception 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 new file mode 100644 index 0000000..43681fd --- /dev/null +++ b/src/bmkg/parsers/parse_weather_forecast_data.py @@ -0,0 +1,136 @@ +from collections.abc import Iterator +from itertools import chain + +# FIXME +# remove `type: ignore` if there is a stub for defusedxml +from defusedxml.ElementTree import fromstring # type: ignore + +from ..enums import Type +from ..exception import WeatherForecastParseError +from ..schemas import ( + Weather, + WeatherForecast, +) +from ..types import ( + WeatherForecastParameter, + WeatherForecastParameterId, + WeatherForecastParameters, +) +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 + +__all__ = ["parse_weather_forecast_data"] + + +def parse_weather_forecast_data(weather_forecast_data: str | bytes) -> 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. + + Args: + weather_forecast_data: string or bytes of xml 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: WeatherForecastParameters = {} + for parameter_element in area_element.iterfind("parameter"): + parameter_id: WeatherForecastParameterId | None = 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"], + ) + ) + + areas[area] = weathers + + return WeatherForecast(data, forecast, issue, areas) diff --git a/src/bmkg/parsers/parse_wind_direction_element.py b/src/bmkg/parsers/parse_wind_direction_element.py new file mode 100644 index 0000000..8e22ff2 --- /dev/null +++ b/src/bmkg/parsers/parse_wind_direction_element.py @@ -0,0 +1,71 @@ +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 ..exception 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 new file mode 100644 index 0000000..0b8abf7 --- /dev/null +++ b/src/bmkg/parsers/parse_wind_speed_element.py @@ -0,0 +1,77 @@ +from collections.abc import Iterator + +# FIXME +# Element is used only for typing not parsing +from xml.etree.ElementTree import Element # nosec B405 + +from ..exception 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/weather_forecast/schemas/__init__.py b/src/bmkg/schemas/__init__.py similarity index 57% rename from src/bmkg/weather_forecast/schemas/__init__.py rename to src/bmkg/schemas/__init__.py index 958a0a7..e370df9 100644 --- a/src/bmkg/weather_forecast/schemas/__init__.py +++ b/src/bmkg/schemas/__init__.py @@ -1,8 +1,14 @@ 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 .shakemap import Shakemap +from .strong_earthquake import StrongEarthquake from .temperature import Temperature from .weather import Weather from .weather_forecast import WeatherForecast @@ -11,10 +17,16 @@ __all__ = [ "Area", + "Coordinate", "Data", + "Earthquake", + "FeltEarthquake", "Forecast", "Humidity", + "LatestEarthquake", "Name", + "Shakemap", + "StrongEarthquake", "Temperature", "WeatherForecast", "Weather", diff --git a/src/bmkg/weather_forecast/schemas/area.py b/src/bmkg/schemas/area.py similarity index 94% rename from src/bmkg/weather_forecast/schemas/area.py rename to src/bmkg/schemas/area.py index 82acccd..1f37802 100644 --- a/src/bmkg/weather_forecast/schemas/area.py +++ b/src/bmkg/schemas/area.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from ...common.schemas import Coordinate from ..enums import Type +from .coordinate import Coordinate from .name import Name __all__ = ["Area"] diff --git a/src/bmkg/common/schemas/coordinate.py b/src/bmkg/schemas/coordinate.py similarity index 100% rename from src/bmkg/common/schemas/coordinate.py rename to src/bmkg/schemas/coordinate.py diff --git a/src/bmkg/weather_forecast/schemas/data.py b/src/bmkg/schemas/data.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/data.py rename to src/bmkg/schemas/data.py diff --git a/src/bmkg/earthquake/schemas/earthquake.py b/src/bmkg/schemas/earthquake.py similarity index 95% rename from src/bmkg/earthquake/schemas/earthquake.py rename to src/bmkg/schemas/earthquake.py index 873dca2..c071939 100644 --- a/src/bmkg/earthquake/schemas/earthquake.py +++ b/src/bmkg/schemas/earthquake.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from datetime import datetime -from ...common.schemas import Coordinate +from .coordinate import Coordinate __all__ = ["Earthquake"] diff --git a/src/bmkg/earthquake/schemas/felt_earthquake.py b/src/bmkg/schemas/felt_earthquake.py similarity index 100% rename from src/bmkg/earthquake/schemas/felt_earthquake.py rename to src/bmkg/schemas/felt_earthquake.py diff --git a/src/bmkg/weather_forecast/schemas/forecast.py b/src/bmkg/schemas/forecast.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/forecast.py rename to src/bmkg/schemas/forecast.py diff --git a/src/bmkg/weather_forecast/schemas/humidity.py b/src/bmkg/schemas/humidity.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/humidity.py rename to src/bmkg/schemas/humidity.py diff --git a/src/bmkg/earthquake/schemas/latest_earthquake.py b/src/bmkg/schemas/latest_earthquake.py similarity index 85% rename from src/bmkg/earthquake/schemas/latest_earthquake.py rename to src/bmkg/schemas/latest_earthquake.py index de197b7..ae96021 100644 --- a/src/bmkg/earthquake/schemas/latest_earthquake.py +++ b/src/bmkg/schemas/latest_earthquake.py @@ -1,7 +1,7 @@ from dataclasses import dataclass -from ..types import Shakemap from .earthquake import Earthquake +from .shakemap import Shakemap __all__ = ["LatestEarthquake"] @@ -15,7 +15,7 @@ class LatestEarthquake: earthquake: earthquake schema. potency: potential tsunami or not, and the status of the earthquake felt. felt: area that felt an earthquake on the MMI scale. - shakemap: shakemap file name of latest earthquake. + shakemap: shakemap schema. """ earthquake: Earthquake diff --git a/src/bmkg/weather_forecast/schemas/name.py b/src/bmkg/schemas/name.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/name.py rename to src/bmkg/schemas/name.py diff --git a/src/bmkg/schemas/shakemap.py b/src/bmkg/schemas/shakemap.py new file mode 100644 index 0000000..7f9bc1b --- /dev/null +++ b/src/bmkg/schemas/shakemap.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +__all__ = ["Shakemap"] + + +@dataclass(slots=True) +class Shakemap: + """ + A schema used to store info about shakemap. + + Attributes: + file_name: name of shakemap file. + """ + + file_name: str diff --git a/src/bmkg/earthquake/schemas/strong_earthquake.py b/src/bmkg/schemas/strong_earthquake.py similarity index 100% rename from src/bmkg/earthquake/schemas/strong_earthquake.py rename to src/bmkg/schemas/strong_earthquake.py diff --git a/src/bmkg/weather_forecast/schemas/temperature.py b/src/bmkg/schemas/temperature.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/temperature.py rename to src/bmkg/schemas/temperature.py diff --git a/src/bmkg/weather_forecast/schemas/weather.py b/src/bmkg/schemas/weather.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/weather.py rename to src/bmkg/schemas/weather.py diff --git a/src/bmkg/weather_forecast/schemas/weather_forecast.py b/src/bmkg/schemas/weather_forecast.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/weather_forecast.py rename to src/bmkg/schemas/weather_forecast.py diff --git a/src/bmkg/weather_forecast/schemas/wind_direction.py b/src/bmkg/schemas/wind_direction.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/wind_direction.py rename to src/bmkg/schemas/wind_direction.py diff --git a/src/bmkg/weather_forecast/schemas/wind_speed.py b/src/bmkg/schemas/wind_speed.py similarity index 100% rename from src/bmkg/weather_forecast/schemas/wind_speed.py rename to src/bmkg/schemas/wind_speed.py diff --git a/src/bmkg/earthquake/types.py b/src/bmkg/types.py similarity index 52% rename from src/bmkg/earthquake/types.py rename to src/bmkg/types.py index 3a2714d..ba8b7e9 100644 --- a/src/bmkg/earthquake/types.py +++ b/src/bmkg/types.py @@ -1,7 +1,18 @@ -from typing import TypedDict +from datetime import datetime +from typing import Iterator, Literal, TypedDict + +from .enums import Weather +from .schemas import ( + Humidity, + Temperature, + WindDirection, + WindSpeed, +) __all__ = [ - "Shakemap", + "WeatherForecastParameter", + "WeatherForecastParameterId", + "WeatherForecastParameters", "EarthquakeData", "FeltEarthquakeData", "LatestEarthquakeData", @@ -11,7 +22,32 @@ "InfoStrongEarthquakeData", ] -Shakemap = str + +class WeatherForecastParameters(TypedDict, total=False): + datetime: Iterator[datetime] + hu: Iterator[Humidity] + humax: Iterator[Humidity] + humin: Iterator[Humidity] + t: Iterator[Temperature] + tmax: Iterator[Temperature] + tmin: Iterator[Temperature] + weather: Iterator[Weather] + wd: Iterator[WindDirection] + ws: Iterator[WindSpeed] + + +WeatherForecastParameter = ( + Iterator[datetime] + | Iterator[Humidity] + | Iterator[Temperature] + | Iterator[Weather] + | Iterator[WindDirection] + | Iterator[WindSpeed] +) + +WeatherForecastParameterId = Literal[ + "datetime", "hu", "humax", "humin", "t", "tmax", "tmin", "weather", "wd", "ws" +] class EarthquakeData(TypedDict): @@ -37,7 +73,7 @@ class FeltEarthquakeData(EarthquakeData): class LatestEarthquakeData(EarthquakeData): Potensi: str Dirasakan: str - Shakemap: Shakemap + Shakemap: str class _StrongEarthquakeData(TypedDict): diff --git a/src/bmkg/weather_forecast/__init__.py b/src/bmkg/weather_forecast/__init__.py deleted file mode 100644 index 6574e10..0000000 --- a/src/bmkg/weather_forecast/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .weather_forecast import WeatherForecast - -__all__ = ["WeatherForecast"] diff --git a/src/bmkg/weather_forecast/exception.py b/src/bmkg/weather_forecast/exception.py deleted file mode 100644 index eabacd5..0000000 --- a/src/bmkg/weather_forecast/exception.py +++ /dev/null @@ -1,11 +0,0 @@ -from ..common.exception import BMKGError - -__all__ = ["WeatherForecastParseError"] - - -class WeatherForecastParseError(BMKGError): - """ - Weather forecast parse exception. - """ - - pass diff --git a/src/bmkg/weather_forecast/parser.py b/src/bmkg/weather_forecast/parser.py deleted file mode 100644 index dfa53cc..0000000 --- a/src/bmkg/weather_forecast/parser.py +++ /dev/null @@ -1,671 +0,0 @@ -from collections.abc import Iterator -from datetime import datetime -from itertools import chain - -# FIXME -# Element is used only for typing not parsing -from xml.etree.ElementTree import Element # nosec B405 - -# FIXME -# remove `type: ignore` if there is a stub for defusedxml -from defusedxml.ElementTree import fromstring # type: ignore - -from ..common.schemas import Coordinate -from .enums import Cardinal, Sexa, Type -from .enums import Weather as WeatherEnum -from .exception import WeatherForecastParseError -from .schemas import ( - Area, - Data, - Forecast, - Humidity, - Name, - Temperature, - Weather, - WeatherForecast, - WindDirection, - WindSpeed, -) -from .types import ( - WeatherForecastParameter, - WeatherForecastParameterId, - WeatherForecastParameters, -) - -__all__ = [ - "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_forecast_data", -] - - -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: - >>> 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) - - -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: - >>> 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) - - -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: - >>> element = fromstring( - ... "" - ... "20240116032347" - ... "2024" - ... "01" - ... "16" - ... "03" - ... "23" - ... "47" - ... "" - ... ) - >>> issue = parse_issue_element(element) - >>> issue - datetime.datetime(2024, 1, 16, 3, 23, 47) - """ - timestamp_element = element.find("timestamp") - if timestamp_element is None: - raise WeatherForecastParseError("timestamp tag in issue tag not found") - - timestamp = timestamp_element.text - if timestamp is None: - raise WeatherForecastParseError("timestamp tag in issue tag has no text") - - return datetime.strptime(timestamp, "%Y%m%d%H%M%S") - - -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: - >>> 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, - ) - - -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: - >>> 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)) - - -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: - >>> 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)) - - -def parse_weather_element(element: Element) -> Iterator[WeatherEnum]: - """ - 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: - >>> 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 not has no text" - ) - - yield WeatherEnum(int(weather)) - - -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: - >>> 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)) - - -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: - >>> 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)) - - -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: - >>> 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") - - -def parse_weather_forecast_data(weather_forecast_data: str | bytes) -> 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. - - Args: - weather_forecast_data: string or bytes of xml 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: WeatherForecastParameters = {} - for parameter_element in area_element.iterfind("parameter"): - parameter_id: WeatherForecastParameterId | None = 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"], - ) - ) - - areas[area] = weathers - - return WeatherForecast(data, forecast, issue, areas) diff --git a/src/bmkg/weather_forecast/types.py b/src/bmkg/weather_forecast/types.py deleted file mode 100644 index c85c68d..0000000 --- a/src/bmkg/weather_forecast/types.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime -from typing import Iterator, Literal, TypedDict - -from .enums import Weather -from .schemas import ( - Humidity, - Temperature, - WindDirection, - WindSpeed, -) - -__all__ = [ - "WeatherForecastParameter", - "WeatherForecastParameterId", - "WeatherForecastParameters", -] - - -class WeatherForecastParameters(TypedDict, total=False): - datetime: Iterator[datetime] - hu: Iterator[Humidity] - humax: Iterator[Humidity] - humin: Iterator[Humidity] - t: Iterator[Temperature] - tmax: Iterator[Temperature] - tmin: Iterator[Temperature] - weather: Iterator[Weather] - wd: Iterator[WindDirection] - ws: Iterator[WindSpeed] - - -WeatherForecastParameter = ( - Iterator[datetime] - | Iterator[Humidity] - | Iterator[Temperature] - | Iterator[Weather] - | Iterator[WindDirection] - | Iterator[WindSpeed] -) - -WeatherForecastParameterId = Literal[ - "datetime", "hu", "humax", "humin", "t", "tmax", "tmin", "weather", "wd", "ws" -] diff --git a/src/bmkg/common/__init__.py b/tests/__init__.py similarity index 100% rename from src/bmkg/common/__init__.py rename to tests/__init__.py diff --git a/tests/test_earthquake/test_integration.py b/tests/test_earthquake/test_integration.py deleted file mode 100644 index a64ada6..0000000 --- a/tests/test_earthquake/test_integration.py +++ /dev/null @@ -1,22 +0,0 @@ -from bmkg.earthquake import Earthquake - - -async def test_when_request_latest_earthquake_then_pass_without_error(): - async with Earthquake() as earthquake: - await earthquake.get_latest_earthquake() - - -async def test_when_request_latest_earthquake_shakemap_then_pass_without_error(): - async with Earthquake() as earthquake: - latest_earthquake = await earthquake.get_latest_earthquake() - await earthquake.get_latest_earthquake_shakemap(latest_earthquake.shakemap) - - -async def test_when_request_strong_earthquake_then_pass_without_error(): - async with Earthquake() as earthquake: - list(await earthquake.get_strong_earthquake()) - - -async def test_when_request_felt_earthquake_then_pass_without_error(): - async with Earthquake() as earthquake: - list(await earthquake.get_felt_earthquake()) diff --git a/tests/test_earthquake_integration.py b/tests/test_earthquake_integration.py new file mode 100644 index 0000000..3a0a338 --- /dev/null +++ b/tests/test_earthquake_integration.py @@ -0,0 +1,22 @@ +from bmkg import BMKG + + +async def test_when_request_latest_earthquake_then_pass_without_error(): + async with BMKG() as bmkg: + await bmkg.earthquake.get_latest_earthquake() + + +async def test_when_request_latest_earthquake_shakemap_then_pass_without_error(): + async with BMKG() as bmkg: + latest_earthquake = await bmkg.earthquake.get_latest_earthquake() + await latest_earthquake.shakemap.get_content() + + +async def test_when_request_strong_earthquake_then_pass_without_error(): + async with BMKG() as bmkg: + list(await bmkg.earthquake.get_strong_earthquake()) + + +async def test_when_request_felt_earthquake_then_pass_without_error(): + async with BMKG() as bmkg: + list(await bmkg.earthquake.get_felt_earthquake()) diff --git a/tests/test_weather_forecast/test_enum.py b/tests/test_enum.py similarity index 98% rename from tests/test_weather_forecast/test_enum.py rename to tests/test_enum.py index 067bf02..a1c35c7 100644 --- a/tests/test_weather_forecast/test_enum.py +++ b/tests/test_enum.py @@ -2,7 +2,7 @@ import pytest -from bmkg.weather_forecast.enums import Cardinal, Province, Sexa, Type, Weather +from bmkg.enums import Cardinal, Province, Sexa, Type, Weather expected_cardinal_values = ( "N", diff --git a/tests/test_earthquake/__init__.py b/tests/test_parsers/__init__.py similarity index 100% rename from tests/test_earthquake/__init__.py rename to tests/test_parsers/__init__.py diff --git a/tests/test_parsers/test_parse_area_element.py b/tests/test_parsers/test_parse_area_element.py new file mode 100644 index 0000000..80685af --- /dev/null +++ b/tests/test_parsers/test_parse_area_element.py @@ -0,0 +1,62 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..4412acf --- /dev/null +++ b/tests/test_parsers/test_parse_data_element.py @@ -0,0 +1,21 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..5468415 --- /dev/null +++ b/tests/test_parsers/test_parse_datetime_element.py @@ -0,0 +1,19 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..6767cae --- /dev/null +++ b/tests/test_parsers/test_parse_forecast_element.py @@ -0,0 +1,16 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..1df1e28 --- /dev/null +++ b/tests/test_parsers/test_parse_humidity_element.py @@ -0,0 +1,35 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..9f3e0f0 --- /dev/null +++ b/tests/test_parsers/test_parse_issue_element.py @@ -0,0 +1,28 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception import WeatherForecastParseError +from bmkg.parsers import parse_issue_element + + +def test_parse_element_with_invalid_attribute(): + element = MagicMock() + element.find.return_value = None + + with pytest.raises( + WeatherForecastParseError, match="timestamp tag in issue tag not found" + ): + parse_issue_element(element) + + +def test_parse_element_with_invalid_humidity_text(): + timestamp = MagicMock() + timestamp.text = None + + element = MagicMock() + element.find.return_value = timestamp + with pytest.raises( + WeatherForecastParseError, match="timestamp tag in issue tag has no text" + ): + parse_issue_element(element) diff --git a/tests/test_parsers/test_parse_temperature_element.py b/tests/test_parsers/test_parse_temperature_element.py new file mode 100644 index 0000000..5b17d8d --- /dev/null +++ b/tests/test_parsers/test_parse_temperature_element.py @@ -0,0 +1,53 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..320b07b --- /dev/null +++ b/tests/test_parsers/test_parse_weather_element.py @@ -0,0 +1,35 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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_wind_direction_element.py b/tests/test_parsers/test_parse_wind_direction_element.py new file mode 100644 index 0000000..bf6445d --- /dev/null +++ b/tests/test_parsers/test_parse_wind_direction_element.py @@ -0,0 +1,54 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 new file mode 100644 index 0000000..9cc99fa --- /dev/null +++ b/tests/test_parsers/test_parse_wind_speed_element.py @@ -0,0 +1,55 @@ +from unittest.mock import MagicMock + +import pytest + +from bmkg.exception 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 diff --git a/tests/test_weather_forecast/__init__.py b/tests/test_weather_forecast/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_weather_forecast/test_integration.py b/tests/test_weather_forecast_integration.py similarity index 55% rename from tests/test_weather_forecast/test_integration.py rename to tests/test_weather_forecast_integration.py index e31d9d2..daab0a2 100644 --- a/tests/test_weather_forecast/test_integration.py +++ b/tests/test_weather_forecast_integration.py @@ -1,15 +1,17 @@ import pytest -from bmkg.weather_forecast import WeatherForecast -from bmkg.weather_forecast.enums import Province, Type +from bmkg import BMKG +from bmkg.enums import Province, Type @pytest.mark.parametrize("province", Province) async def test_given_provinces_when_request_weather_forecast_then_pass_without_error( province, ): - async with WeatherForecast() as weather_forecast: - weather_forecast_data = await weather_forecast.get_weather_forecast(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)