気象庁API解体新書

## 参考ページ
https://github.com/misohena/el-jma/blob/main/docs/how-to-get-jma-forecast.org

## はじめに
仕事で気象庁APIを取り扱った。政府ドメイン（goドメイン）で信頼はできそうだが、クセが強いうえにまともにドキュメントがないのでまとめる。これは共有すべきナレッジだな

https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2820100

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/879868a3-4898-5f66-716b-6e6e7ca1bda1.png)

## データの観察とマスタデータの作成
まずはマスタをつくっていく。このプログラムはいわゆるシーダーデータを作成し、その後に控える予報データ取得プログラムが、それを参照し、データを処理していく（以下のurlを見ると `.../const/...` と書いてあるね）

`https://www.jma.go.jp/bosai/common/const/area.json`
`https://www.jma.go.jp/bosai/forecast/const/forecast_area.json`
`https://www.jma.go.jp/bosai/amedas/const/amedastable.json`

|プロパティ|階層|
|:----|:----|
|centers|地方予報区(11区)|
|offices|府県予報区(58区)|
|class10s|一次細分区域(142区)|
|class15s|市町村等をまとめた地域|
|class20s|二次細分区域|

### jma_area
いわゆる地方区分。生データでは `center` という名前で取り扱われているので、`area` と名付けた。
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/b06e85a1-8fe6-85c1-313e-e5f23a03dadf.png">

|code|name|
|:----|:----|
|010600|近畿地方|

### jma_prefecture
いわゆる都道府県（北海道と沖縄が、より分割されている）。生データでは `office` という名前で取り扱われているので、`prefecture` と名付けた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/60973aed-df41-b1d0-90ac-417fa755a247.png)

|code|jma_area_id|name|
|:----|:----|:----|
|250000|010600|滋賀県|
|260000|010600|京都府|
|270000|010600|大阪府|
|280000|010600|兵庫県|
|290000|010600|奈良県|
|300000|010600|和歌山県|

### jma_region
[ひとつの都道府県の、ある範囲（北部とか南部）](https://www.jma.go.jp/bosai/map.html#9/35.09/135.363/&contents=forecast)。生データでは `class10` という名前で取り扱われているので `region` と名付けた。

:::note info
この region がキモの概念になる
:::


<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/78037053-c041-fbc4-f2ef-ae589c4e2b8f.png"><br>

|code|jma_prefecture_id|name|
|:----|:----|:----|
|280010|280000|南部|
|280020|280000|北部|

### jma_city
ひとつの都道府県のひとつの市区町村（一部市町村を分割している）。生データでは `class20` という名前で取り扱われているので `city` と名付けた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ff06fc5d-897e-160e-fd57-d562bbfda96d.png)

|no.|code|jma_region_id|name|
|:----|:----|:----|:----|
|1|2810000|280010|神戸市|
|2|2820200|280010|尼崎市|
|3|2820400|280010|西宮市|
|4|2820600|280010|芦屋市|
|5|2820700|280010|伊丹市|
|6|2821400|280010|宝塚市|
|7|2821700|280010|川西市|
|8|2821900|280010|三田市|
|9|2830100|280010|猪名川町|
|10|2821300|280010|西脇市|
|11|2822100|280010|丹波篠山市|
|12|2822300|280010|丹波市|
|13|2836500|280010|多可町|
|14|2822700|280010|宍粟市|
|15|2844200|280010|市川町|
|16|2844300|280010|福崎町|
|17|2844600|280010|神河町|
|18|2850100|280010|佐用町|
|19|2820300|280010|明石市|
|20|2821000|280010|加古川市|
|21|2821500|280010|三木市|
|22|2821600|280010|高砂市|
|23|2821800|280010|小野市|
|24|2822000|280010|加西市|
|25|2822800|280010|加東市|
|26|2838100|280010|稲美町|
|27|2838200|280010|播磨町|
|28|2820100|280010|姫路市|
|29|2820800|280010|相生市|
|30|2821200|280010|赤穂市|
|31|2822900|280010|たつの市|
|32|2846400|280010|太子町|
|33|2848100|280010|上郡町|
|34|2820500|280010|洲本市|
|35|2822400|280010|南あわじ市|
|36|2822600|280010|淡路市|

:::note
`class15（いくつかのcityの集まり）`もあるんだけど、いわゆる「郡」かと思われる（但馬北部など）。これは普段づかいしないので親子関係を紐づけるとき、内部的に使いながらもテーブルのデータにはしない（郡を省略する）ことにする
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/5d3f07ac-80b0-7f80-4336-0fa0502abace.png">
:::

### jma_amedas
[いわゆる気象観測所](https://www.data.jma.go.jp/sendai/knowledge/kyouiku/sokki/kansoku/amds.pdf)。生データでは `amedas` という名前で取り扱われているのでそのまま `amedas` と名付けた。

|id|jma_region_id|
|:----|:----|
|63518|280010|
|63576|280010|
|63571|280010|
|63383|280010|

### ER図
regionが肝ってことやな。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/256f28b0-da42-a99c-38d0-fda028e8d38d.png)

## クセ強ポイント
### amedasのグルーピングについて
- 市区町村別のアメダスリスト `forecast_area.json` はリージョン単位でグルーピングされている
- region に複数のアメダスがある場合、代表cityがある
- ということは amedas が city へ紐づかないといけないが、class10（＝region）へ紐づける

```json:forecast_area.json
{
    :
  "280000": [
    {
      "class10": "280010",  ←region: 南部
      "amedas": [
        "63518",  ←amedas: 神戸
        "63576",  ←amedas: （欠番！）
        "63571",  ←amedas: 洲本
        "63383"  ←amedas: 姫路
      ],
      "class20": "2810000"  ←city: 神戸市（南部代表都市）
    },
    {
      "class10": "280020",  ←region: 北部
      "amedas": [
        "63051"  ←amedas: 豊岡
      ],
      "class20": "2820900"  ←city: 豊岡市（北部代表都市）
    }
  ]
      :
}
```

### 予報データについて
- 天気予報ファイルは prefecture で取得する（例: `280000.json`）
    - 気温情報がアメダス単位で収録されている（アメダスは region に紐づいている）
    - アプリ側のターゲットとなる建物等の住所から region を特定できる
    - 神戸は南部なので、せめて兵庫県南部の気温を抽出したい
- 「本日」の予報データは時間の経過とともに値の要素数が減っていく（あたりまえっちゃあたりまえか...）

```json:280000.json（できれば市区町村単位で天気を取得したい）
[
  {
    "publishingOffice": "神戸地方気象台",
    "reportDatetime": "2024-03-28T17:00:00+09:00",
    "timeSeries": [
      {
        "areas": [
          {
            "area": {"name": "神戸", "code": "63518"},  ←region: 南部
            "temps": ["13", "19"]
          },
          {
            "area": {"name": "豊岡", "code": "63051"},  ←region: 北部
            "temps": ["12", "22"]
          },
          {
            "area": {"name": "洲本", "code": "63571"},  ←region: 南部
            "temps": ["14", "20"]
          },
          {
            "area": {"name": "姫路", "code": "63383"},  ←region: 南部
            "temps": ["14", "21"]
          }
        ]
      }
    ]
  }
      :
]
```

### amedasマスタについて
- アメダスidは市区町村と紐づかない　←:interrobang:

```json:amedastable.json（アメダスの情報から市区町村と紐づかない。。。）
{
  "63051": {
    "type": "B",
    "elems": "11111111",
    "lat": [35, 32.1],
    "lon": [134, 49.3],
    "alt": 3,
    "kjName": "豊岡",
    "knName": "トヨオカ",
    "enName": "Toyooka"
  },
  "63518": {
    "type": "A",
    "elems": "11111111",
    "lat": [34, 41.8],
    "lon": [135, 12.7],
    "alt": 5,
    "kjName": "神戸",
    "knName": "コウベ",
    "enName": "Kobe"
  },
  "63571": {
    "type": "B",
    "elems": "11111011",
    "lat": [34, 18.6],
    "lon": [134, 50.9],
    "alt": 69,
    "kjName": "洲本",
    "knName": "スモト",
    "enName": "Sumoto"
  },
  "63383": {
    "type": "B",
    "elems": "11111011",
    "lat": [34, 50.3],
    "lon": [134, 40.2],
    "alt": 38,
    "kjName": "姫路",
    "knName": "ヒメジ",
    "enName": "Himeji"
  }
}
```

### 気象庁データについて
https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2810000

もう一度この絵を見てみよう。姫路市を選んで表示しても神戸市を選んで表示しても3つの都市が必ず表示される:thinking:。そして、表示された3つの都市以外の兵庫県南部の都市（南あわじ市 など）を表示してもこの3つの代表都市が表示される:thinking:。ということはアメダスと市区町村は紐づかず（アメダスマスタ全量の天気データはない）、結局リージョン単位なんだなということがわかる。それ以上のデータがほしければ金払って民間APIだね。

|姫路市|南あわじ市|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/5d1f6e41-10ec-defd-2c2e-d451123fa5f0.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/b0dbe526-0526-6170-3b2a-39fd47fed4fa.png)|

## アプリからの使われかたを考える
アプリにおける使われ方としては、建物Aを登録するときに、市区町村を気象庁APIベースのマスタからドロップダウンで選択させるのがいいだろうな。

## マスタを保存する仕組みを作ろう
### create root directory
```console:console
mkdir jma_weather
cd jma_weather
```

### venv
```console:console
python -m venv venv311
```

### create project
```console:console
pip install django
django-admin startproject config .
```

### create app
```console:console
python manage.py startapp weather
```

### update settings
```diff_python:config/settings.py
INSTALLED_APPS = [
        :
+   "weather",
]
```

### create model
```python:weather/models.py
from django.db import models

class JmaArea(models.Model):
    """
    an area in the JMA
    生データでは center という名前で取り扱われている

    Attributes:
        code (str): エリアコード
        name (str): エリア名
    """

    code = models.CharField(unique=True, max_length=6)
    name = models.CharField(max_length=100)


class JmaPrefecture(models.Model):
    """
    a prefecture in the JMA
    生データでは office という名前で取り扱われている

    Attributes:
        code (CharField): 都道府県コード
        jma_area (ForeignKey): FK to JmaArea
        name (CharField): 都道府県名
    """

    code = models.CharField(unique=True, max_length=6)
    jma_area = models.ForeignKey(JmaArea, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)


class JmaRegion(models.Model):
    """
    a region in the JMA
    生データでは class10 という名前で取り扱われている

    Attributes:
        code (str): リージョンコード
        jma_prefecture (JmaPrefecture): FK to JmaPrefecture
        name (str): リージョン名
    """

    code = models.CharField(unique=True, max_length=6)
    jma_prefecture = models.ForeignKey(JmaPrefecture, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)


class JmaCity(models.Model):
    """
    a city in the JMA
    生データでは class20 という名前で取り扱われている

    Attributes:
        code (str): 市区町村コード
        jma_region (JmaRegion): FK to JMA region
        name (str): 市区町村名
    """

    code = models.CharField(unique=True, max_length=7)
    jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)


class JmaAmedas(models.Model):
    """
    a AMeDas in the JMA

    Attributes:
        code (str): アメダス観測所コード
        jma_region (JmaRegion): FK to JMA region
    """

    code = models.CharField(unique=True, max_length=5)
    jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)


class JmaWeatherCode(models.Model):
    """
    天気コードマスタ

    Attributes:
        code (CharField): "123"
        summary_code (CharField): "100"
        image (FileField): "100.svg"
        name (CharField): "晴"
        name_en (CharField): "CLEAR, FREQUENT SNOW FLURRIES LATER"
    """

    code = models.CharField(unique=True, max_length=3)
    summary_code = models.CharField(max_length=3)
    image = models.CharField(max_length=7)
    name = models.CharField(max_length=20)
    name_en = models.CharField(max_length=100)


class JmaWeather(models.Model):
    """
    日々の天気（1時間ごとにバッチで取得）

    See Also: https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2820100

    Attributes:
        jma_region (JmaRegion): A ForeignKey field representing the region for which the weather data is recorded.
        reporting_date (DateField): A DateField representing the date on which the weather data is reported.
        jma_weather_code (JmaWeatherCode): A ForeignKey field representing the weather code for the recorded data.
        weather_text (CharField): A CharField representing the text description of the weather.
        wind_text (CharField): A CharField representing the text description of the wind conditions.
        wave_text (CharField): A CharField representing the text description of the wave conditions.
        avg_rain_probability (FloatField, optional): 降水確率
        avg_min_temperature (FloatField, optional): 最低気温
        avg_max_temperature (FloatField, optional): 最高気温
        avg_max_wind_speed (FloatField, optional): 最大風速

    """

    jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
    reporting_date = models.DateField()
    jma_weather_code = models.ForeignKey(JmaWeatherCode, on_delete=models.CASCADE)
    weather_text = models.CharField(max_length=255)
    wind_text = models.CharField(max_length=255)
    wave_text = models.CharField(max_length=255)
    avg_rain_probability = models.FloatField(null=True)
    avg_min_temperature = models.FloatField(null=True)
    avg_max_temperature = models.FloatField(null=True)
    avg_max_wind_speed = models.FloatField(null=True)

    class Meta:
        unique_together = (("jma_region", "reporting_date"),)

class JmaWarning(models.Model):
    """
    Model representing a JMA warning.

    See Also: https://www.jma.go.jp/bosai/warning/#area_type=class20s&area_code=2810000&lang=ja

    Attributes:
        jma_region (ForeignKey): The JMA region associated with the warning.
        warnings (CharField): The description of the warning.
    """

    jma_region = models.ForeignKey(JmaRegion, on_delete=models.CASCADE)
    warnings = models.CharField(max_length=100)

    class Meta:
        unique_together = ("jma_region",)

```

### migration
```
python manage.py makemigrations weather
python manage.py migrate
```

### create vo
```py:weather/domain/valueobject/weather/jma.py
from dataclasses import dataclass


# マスタデータ
@dataclass
class JmaConstWeatherCode:
    """
    気象庁の定数マスタ（天気コード）

    Attributes:
        code (str): The code of the weather.
        image_day (str): The image path for day weather.
        image_night (str): The image path for night weather.
        summary_code (str): The summary code for the weather.
        name (str): The name of the weather.
        name_en (str): The English name of the weather.
    """

    code: str
    image_day: str
    image_night: str
    summary_code: str
    name: str
    name_en: str


@dataclass
class JmaConstGeographicArea:
    """
    気象庁の定数マスタ（area, prefecture, region, city_group, city）

    Attributes:
        code (str): The code of the constant.
        name (str): The name of the constant.
        children (list[str]): The list of children constants' codes.
        parent (str): The code of the parent constant.
    """

    code: str
    name: str
    children: list[str]
    parent: str


# 予報データ
class MeanCalculable:
    """
    平均を出すための生値とその平均値

    Args:
        float_list (list[float]): 生値
    """

    def __init__(self, float_list: list[float]) -> None:
        self.raw: list[float] = float_list
        self.mean: float = (
            round(sum(float_list) / len(float_list), 1) if float_list else 0
        )


@dataclass(frozen=True)
class Region:
    """
    A class representing a region.

    Attributes:
        code (str): "280010"
        name (str): "南部"
    """

    code: str
    name: str


@dataclass(frozen=True)
class SummaryText:
    """
    気象情報: サマリー情報

    Attributes:
        weather (str): "雨　所により　雷を伴い　非常に　激しく　降る"
        wind (str): "東の風　やや強く　海上　では　東の風　強く"
        wave (str): "１．５メートル　ただし　淡路島南部　では　３メートル　うねり　を伴う"
    """

    weather: str
    wind: str
    wave: str


@dataclass(frozen=True)
class RainData:
    """
    気象情報: 降水確率

    Attributes:
        values (MeanCalculable): 生値とその平均値（時間帯平均）
        unit (str): "%"
    """

    values: MeanCalculable
    unit: str = "%"


@dataclass(frozen=True)
class TemperatureData:
    """
    気象情報: 気温

    Attributes:
        min_values (MeanCalculable): 生値とその平均値（region平均）
        max_values (MeanCalculable): 生値とその平均値（region平均）
        unit (str): "℃"
    """

    min_values: MeanCalculable
    max_values: MeanCalculable
    unit: str = "℃"


class WindData:
    """
    気象情報: 最大風速

    Attributes:
        values (MeanCalculable): 生値とその平均値（時間帯平均）
        unit (str): "以下" or "メートル毎秒"

    Notes: 風速値が9のばあい、その object に condition というキーが現れて "以下" という値が入るが
     表現を間引いて、平均値が9以下だったら "以下" にすることにした
    """

    def __init__(self, values: MeanCalculable):
        self.values = values
        self.unit = "以下" if self.values.mean <= 9 else "メートル毎秒"

    def __str__(self):
        return f"最大風速（時間帯平均）は {self.values.mean} {self.unit}"


@dataclass(frozen=True)
class WarningData:
    """
    A class representing warning data.

    Attributes:
        code (str): 14
        status (str): 継続
    """

    code: str
    status: str

```

### create repository
```py:weather/domain/repository/weather/jma.py
from weather.models import JmaPrefecture, JmaRegion, JmaAmedas


class JmaRepository:
    @staticmethod
    def get_amedas_code_list(prefecture_id: str, special_add_region_ids: dict) -> dict:
        amedas_code_in_region = {}
        jma_regions = JmaRegion.objects.filter(
            jma_prefecture=JmaPrefecture.objects.get(code=prefecture_id)
        ).prefetch_related("jmaamedas_set")
        for region in jma_regions:
            amedas_code_in_region[region.code] = [
                amedas.code for amedas in region.jmaamedas_set.all()
            ]
        if prefecture_id in special_add_region_ids:
            region = JmaRegion.objects.get(code=special_add_region_ids[prefecture_id])
            special_add_region_code = special_add_region_ids[prefecture_id]
            amedas_code_in_region[special_add_region_code] = list(
                JmaAmedas.objects.filter(jma_region=region).values_list(
                    "code", flat=True
                )
            )

        return amedas_code_in_region
        
```

### create test
```py:weather/tests/domain/valueobject/weather/test_jma.py
from datetime import datetime
from unittest import mock

from django.test import TestCase

from weather.management.commands import fetch_weather_forecast
from weather.management.commands.fetch_weather_forecast import (
    get_data,
    get_indexes,
)


class TestFetchWeatherForecast(TestCase):
    def setUp(self):
        # 天気予報取得バッチの 十勝地方、奄美地方 変換
        self.func = fetch_weather_forecast.update_prefecture_ids

    def test_update_prefecture_ids_with_invalid_id(self):
        prefecture_ids = ["014030", "032010", "082050", "460040"]
        updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)

        self.assertListEqual(
            updated_prefecture_ids, ["032010", "082050", "014100", "460100"]
        )
        self.assertEqual(
            special_add_region_ids, {"014100": "014030", "460100": "460040"}
        )

    def test_update_prefecture_ids_without_invalid_id(self):
        prefecture_ids = ["014100", "032010", "082050", "460100"]
        updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)

        self.assertListEqual(
            updated_prefecture_ids, ["014100", "032010", "082050", "460100"]
        )
        self.assertEqual(special_add_region_ids, {})

    def test_update_prefecture_ids_with_proxy_id(self):
        #  [("014030", "014100"), ("460040", "460100")]
        prefecture_ids = [
            "014030",
            "032010",
            "082050",
            "460040",
            "014100",
            "460100",
        ]
        updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)

        self.assertListEqual(
            updated_prefecture_ids, ["032010", "082050", "014100", "460100"]
        )
        self.assertEqual(
            special_add_region_ids, {"014100": "014030", "460100": "460040"}
        )

    def test_update_prefecture_ids_with_empty_list(self):
        prefecture_ids = []
        updated_prefecture_ids, special_add_region_ids = self.func(prefecture_ids)

        self.assertListEqual(updated_prefecture_ids, [])
        self.assertEqual(special_add_region_ids, {})


class TestGetDataAndIndexes(TestCase):
    THREE_DAYS = 0
    TYPE_OVERVIEW = 0

    def setUp(self):
        self.target_date = datetime.strptime("2024-08-31", "%Y-%m-%d").date()
        self.mock_response_data = [
            {
                # This corresponds to the THREE_DAYS constant
                "timeSeries": [
                    {
                        "timeDefines": [
                            "2024-08-30T17:00:00+09:00",
                            "2024-08-31T00:00:00+09:00",
                            "2024-09-01T00:00:00+09:00",
                        ],
                        "areas": [{"area": {"name": "南部", "code": "280010"}}],
                    },
                    "dummy POPS",
                    "dummy TEMP",
                ]
            },
            "dummy a week forecast",
        ]
        self.mock_response = mock.MagicMock()
        self.mock_response.status_code = 200
        self.mock_response.json.return_value = self.mock_response_data

    @mock.patch("requests.get")
    def test_get_data(self, mock_requests_get):
        # Mocking requests.get to return the prepared data
        mock_requests_get.return_value = self.mock_response

        url = "http://test.url"  # This url doesn't matter as we mock the requests.get
        data_time_series = get_data(url)

        self.assertEqual(
            self.mock_response_data[TestGetDataAndIndexes.THREE_DAYS]["timeSeries"],
            data_time_series,
        )

    def test_get_indexes(self):
        indexes = get_indexes(
            data_time_defines=self.mock_response_data[TestGetDataAndIndexes.THREE_DAYS][
                "timeSeries"
            ][TestGetDataAndIndexes.TYPE_OVERVIEW]["timeDefines"],
            desired_date=self.target_date,
        )

        self.assertEqual(indexes, [1])
        
```

### create import_weather_master_manual.py
```py:weather/management/commands/import_weather_master_manual.py
import os
import shutil

import requests
from django.core.management.base import BaseCommand

from soil_analysis.domain.valueobject.weather.jma import JmaConstWeatherCode
from soil_analysis.models import (
    JmaWeatherCode,
)


class Command(BaseCommand):
    help = "Import jma weather code master from manual data."

    def handle(self, *args, **options):
        """
        任意の都市の天気ページの `ページのソースを表示` から `TELOPS=` でページ内検索して data に当て込む
        https://www.jma.go.jp/bosai/forecast/#area_type=class20s&area_code=2810000

        Args:
            *args: Additional positional arguments.
            **options: Additional keyword arguments.
        """
        data = {
            100: ["100.svg", "500.svg", "100", "\u6674", "CLEAR"],
            101: [
                "101.svg",
                "501.svg",
                "100",
                "\u6674\u6642\u3005\u66C7",
                "PARTLY CLOUDY",
            ],
            102: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u4E00\u6642\u96E8",
                "CLEAR, OCCASIONAL SCATTERED SHOWERS",
            ],
            103: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u6642\u3005\u96E8",
                "CLEAR, FREQUENT SCATTERED SHOWERS",
            ],
            104: [
                "104.svg",
                "504.svg",
                "400",
                "\u6674\u4E00\u6642\u96EA",
                "CLEAR, SNOW FLURRIES",
            ],
            105: [
                "104.svg",
                "504.svg",
                "400",
                "\u6674\u6642\u3005\u96EA",
                "CLEAR, FREQUENT SNOW FLURRIES",
            ],
            106: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u4E00\u6642\u96E8\u304B\u96EA",
                "CLEAR, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES",
            ],
            107: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u6642\u3005\u96E8\u304B\u96EA",
                "CLEAR, FREQUENT SCATTERED SHOWERS OR SNOW FLURRIES",
            ],
            108: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u4E00\u6642\u96E8\u304B\u96F7\u96E8",
                "CLEAR, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER",
            ],
            110: [
                "110.svg",
                "510.svg",
                "100",
                "\u6674\u5F8C\u6642\u3005\u66C7",
                "CLEAR, PARTLY CLOUDY LATER",
            ],
            111: [
                "110.svg",
                "510.svg",
                "100",
                "\u6674\u5F8C\u66C7",
                "CLEAR, CLOUDY LATER",
            ],
            112: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5F8C\u4E00\u6642\u96E8",
                "CLEAR, OCCASIONAL SCATTERED SHOWERS LATER",
            ],
            113: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5F8C\u6642\u3005\u96E8",
                "CLEAR, FREQUENT SCATTERED SHOWERS LATER",
            ],
            114: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5F8C\u96E8",
                "CLEAR,RAIN LATER",
            ],
            115: [
                "115.svg",
                "515.svg",
                "400",
                "\u6674\u5F8C\u4E00\u6642\u96EA",
                "CLEAR, OCCASIONAL SNOW FLURRIES LATER",
            ],
            116: [
                "115.svg",
                "515.svg",
                "400",
                "\u6674\u5F8C\u6642\u3005\u96EA",
                "CLEAR, FREQUENT SNOW FLURRIES LATER",
            ],
            117: [
                "115.svg",
                "515.svg",
                "400",
                "\u6674\u5F8C\u96EA",
                "CLEAR,SNOW LATER",
            ],
            118: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5F8C\u96E8\u304B\u96EA",
                "CLEAR, RAIN OR SNOW LATER",
            ],
            119: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5F8C\u96E8\u304B\u96F7\u96E8",
                "CLEAR, RAIN AND/OR THUNDER LATER",
            ],
            120: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u671D\u5915\u4E00\u6642\u96E8",
                "OCCASIONAL SCATTERED SHOWERS IN THE MORNING AND EVENING, CLEAR DURING THE DAY",
            ],
            121: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u671D\u306E\u5185\u4E00\u6642\u96E8",
                "OCCASIONAL SCATTERED SHOWERS IN THE MORNING, CLEAR DURING THE DAY",
            ],
            122: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5915\u65B9\u4E00\u6642\u96E8",
                "CLEAR, OCCASIONAL SCATTERED SHOWERS IN THE EVENING",
            ],
            123: [
                "100.svg",
                "500.svg",
                "100",
                "\u6674\u5C71\u6CBF\u3044\u96F7\u96E8",
                "CLEAR IN THE PLAINS, RAIN AND THUNDER NEAR MOUTAINOUS AREAS",
            ],
            124: [
                "100.svg",
                "500.svg",
                "100",
                "\u6674\u5C71\u6CBF\u3044\u96EA",
                "CLEAR IN THE PLAINS, SNOW NEAR MOUTAINOUS AREAS",
            ],
            125: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5348\u5F8C\u306F\u96F7\u96E8",
                "CLEAR, RAIN AND THUNDER IN THE AFTERNOON",
            ],
            126: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u663C\u9803\u304B\u3089\u96E8",
                "CLEAR, RAIN IN THE AFTERNOON",
            ],
            127: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u5915\u65B9\u304B\u3089\u96E8",
                "CLEAR, RAIN IN THE EVENING",
            ],
            128: [
                "112.svg",
                "512.svg",
                "300",
                "\u6674\u591C\u306F\u96E8",
                "CLEAR, RAIN IN THE NIGHT",
            ],
            130: [
                "100.svg",
                "500.svg",
                "100",
                "\u671D\u306E\u5185\u9727\u5F8C\u6674",
                "FOG IN THE MORNING, CLEAR LATER",
            ],
            131: [
                "100.svg",
                "500.svg",
                "100",
                "\u6674\u660E\u3051\u65B9\u9727",
                "FOG AROUND DAWN, CLEAR LATER",
            ],
            132: [
                "101.svg",
                "501.svg",
                "100",
                "\u6674\u671D\u5915\u66C7",
                "CLOUDY IN THE MORNING AND EVENING, CLEAR DURING THE DAY",
            ],
            140: [
                "102.svg",
                "502.svg",
                "300",
                "\u6674\u6642\u3005\u96E8\u3067\u96F7\u3092\u4F34\u3046",
                "CLEAR, FREQUENT SCATTERED SHOWERS AND THUNDER",
            ],
            160: [
                "104.svg",
                "504.svg",
                "400",
                "\u6674\u4E00\u6642\u96EA\u304B\u96E8",
                "CLEAR, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS",
            ],
            170: [
                "104.svg",
                "504.svg",
                "400",
                "\u6674\u6642\u3005\u96EA\u304B\u96E8",
                "CLEAR, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS",
            ],
            181: [
                "115.svg",
                "515.svg",
                "400",
                "\u6674\u5F8C\u96EA\u304B\u96E8",
                "CLEAR, SNOW OR RAIN LATER",
            ],
            200: ["200.svg", "200.svg", "200", "\u66C7", "CLOUDY"],
            201: [
                "201.svg",
                "601.svg",
                "200",
                "\u66C7\u6642\u3005\u6674",
                "MOSTLY CLOUDY",
            ],
            202: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u4E00\u6642\u96E8",
                "CLOUDY, OCCASIONAL SCATTERED SHOWERS",
            ],
            203: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u6642\u3005\u96E8",
                "CLOUDY, FREQUENT SCATTERED SHOWERS",
            ],
            204: [
                "204.svg",
                "204.svg",
                "400",
                "\u66C7\u4E00\u6642\u96EA",
                "CLOUDY, OCCASIONAL SNOW FLURRIES",
            ],
            205: [
                "204.svg",
                "204.svg",
                "400",
                "\u66C7\u6642\u3005\u96EA",
                "CLOUDY FREQUENT SNOW FLURRIES",
            ],
            206: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u4E00\u6642\u96E8\u304B\u96EA",
                "CLOUDY, OCCASIONAL SCATTERED SHOWERS OR SNOW FLURRIES",
            ],
            207: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u6642\u3005\u96E8\u304B\u96EA",
                "CLOUDY, FREQUENT SCCATERED SHOWERS OR SNOW FLURRIES",
            ],
            208: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u4E00\u6642\u96E8\u304B\u96F7\u96E8",
                "CLOUDY, OCCASIONAL SCATTERED SHOWERS AND/OR THUNDER",
            ],
            209: ["200.svg", "200.svg", "200", "\u9727", "FOG"],
            210: [
                "210.svg",
                "610.svg",
                "200",
                "\u66C7\u5F8C\u6642\u3005\u6674",
                "CLOUDY, PARTLY CLOUDY LATER",
            ],
            211: [
                "210.svg",
                "610.svg",
                "200",
                "\u66C7\u5F8C\u6674",
                "CLOUDY, CLEAR LATER",
            ],
            212: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5F8C\u4E00\u6642\u96E8",
                "CLOUDY, OCCASIONAL SCATTERED SHOWERS LATER",
            ],
            213: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5F8C\u6642\u3005\u96E8",
                "CLOUDY, FREQUENT SCATTERED SHOWERS LATER",
            ],
            214: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5F8C\u96E8",
                "CLOUDY, RAIN LATER",
            ],
            215: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u5F8C\u4E00\u6642\u96EA",
                "CLOUDY, SNOW FLURRIES LATER",
            ],
            216: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u5F8C\u6642\u3005\u96EA",
                "CLOUDY, FREQUENT SNOW FLURRIES LATER",
            ],
            217: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u5F8C\u96EA",
                "CLOUDY, SNOW LATER",
            ],
            218: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5F8C\u96E8\u304B\u96EA",
                "CLOUDY, RAIN OR SNOW LATER",
            ],
            219: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5F8C\u96E8\u304B\u96F7\u96E8",
                "CLOUDY, RAIN AND/OR THUNDER LATER",
            ],
            220: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u671D\u5915\u4E00\u6642\u96E8",
                "OCCASIONAL SCCATERED SHOWERS IN THE MORNING AND EVENING, CLOUDY DURING THE DAY",
            ],
            221: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u671D\u306E\u5185\u4E00\u6642\u96E8",
                "CLOUDY OCCASIONAL SCCATERED SHOWERS IN THE MORNING",
            ],
            222: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5915\u65B9\u4E00\u6642\u96E8",
                "CLOUDY, OCCASIONAL SCCATERED SHOWERS IN THE EVENING",
            ],
            223: [
                "201.svg",
                "601.svg",
                "200",
                "\u66C7\u65E5\u4E2D\u6642\u3005\u6674",
                "CLOUDY IN THE MORNING AND EVENING, PARTLY CLOUDY DURING THE DAY,",
            ],
            224: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u663C\u9803\u304B\u3089\u96E8",
                "CLOUDY, RAIN IN THE AFTERNOON",
            ],
            225: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u5915\u65B9\u304B\u3089\u96E8",
                "CLOUDY, RAIN IN THE EVENING",
            ],
            226: [
                "212.svg",
                "212.svg",
                "300",
                "\u66C7\u591C\u306F\u96E8",
                "CLOUDY, RAIN IN THE NIGHT",
            ],
            228: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u663C\u9803\u304B\u3089\u96EA",
                "CLOUDY, SNOW IN THE AFTERNOON",
            ],
            229: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u5915\u65B9\u304B\u3089\u96EA",
                "CLOUDY, SNOW IN THE EVENING",
            ],
            230: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u591C\u306F\u96EA",
                "CLOUDY, SNOW IN THE NIGHT",
            ],
            231: [
                "200.svg",
                "200.svg",
                "200",
                "\u66C7\u6D77\u4E0A\u6D77\u5CB8\u306F\u9727\u304B\u9727\u96E8",
                "CLOUDY, FOG OR DRIZZLING ON THE SEA AND NEAR SEASHORE",
            ],
            240: [
                "202.svg",
                "202.svg",
                "300",
                "\u66C7\u6642\u3005\u96E8\u3067\u96F7\u3092\u4F34\u3046",
                "CLOUDY, FREQUENT SCCATERED SHOWERS AND THUNDER",
            ],
            250: [
                "204.svg",
                "204.svg",
                "400",
                "\u66C7\u6642\u3005\u96EA\u3067\u96F7\u3092\u4F34\u3046",
                "CLOUDY, FREQUENT SNOW AND THUNDER",
            ],
            260: [
                "204.svg",
                "204.svg",
                "400",
                "\u66C7\u4E00\u6642\u96EA\u304B\u96E8",
                "CLOUDY, SNOW FLURRIES OR OCCASIONAL SCATTERED SHOWERS",
            ],
            270: [
                "204.svg",
                "204.svg",
                "400",
                "\u66C7\u6642\u3005\u96EA\u304B\u96E8",
                "CLOUDY, FREQUENT SNOW FLURRIES OR SCATTERED SHOWERS",
            ],
            281: [
                "215.svg",
                "215.svg",
                "400",
                "\u66C7\u5F8C\u96EA\u304B\u96E8",
                "CLOUDY, SNOW OR RAIN LATER",
            ],
            300: ["300.svg", "300.svg", "300", "\u96E8", "RAIN"],
            301: [
                "301.svg",
                "701.svg",
                "300",
                "\u96E8\u6642\u3005\u6674",
                "RAIN, PARTLY CLOUDY",
            ],
            302: [
                "302.svg",
                "302.svg",
                "300",
                "\u96E8\u6642\u3005\u6B62\u3080",
                "SHOWERS THROUGHOUT THE DAY",
            ],
            303: [
                "303.svg",
                "303.svg",
                "400",
                "\u96E8\u6642\u3005\u96EA",
                "RAIN,FREQUENT SNOW FLURRIES",
            ],
            304: ["300.svg", "300.svg", "300", "\u96E8\u304B\u96EA", "RAINORSNOW"],
            306: ["300.svg", "300.svg", "300", "\u5927\u96E8", "HEAVYRAIN"],
            308: [
                "308.svg",
                "308.svg",
                "300",
                "\u96E8\u3067\u66B4\u98A8\u3092\u4F34\u3046",
                "RAINSTORM",
            ],
            309: [
                "303.svg",
                "303.svg",
                "400",
                "\u96E8\u4E00\u6642\u96EA",
                "RAIN,OCCASIONAL SNOW",
            ],
            311: [
                "311.svg",
                "711.svg",
                "300",
                "\u96E8\u5F8C\u6674",
                "RAIN,CLEAR LATER",
            ],
            313: [
                "313.svg",
                "313.svg",
                "300",
                "\u96E8\u5F8C\u66C7",
                "RAIN,CLOUDY LATER",
            ],
            314: [
                "314.svg",
                "314.svg",
                "400",
                "\u96E8\u5F8C\u6642\u3005\u96EA",
                "RAIN, FREQUENT SNOW FLURRIES LATER",
            ],
            315: ["314.svg", "314.svg", "400", "\u96E8\u5F8C\u96EA", "RAIN,SNOW LATER"],
            316: [
                "311.svg",
                "711.svg",
                "300",
                "\u96E8\u304B\u96EA\u5F8C\u6674",
                "RAIN OR SNOW, CLEAR LATER",
            ],
            317: [
                "313.svg",
                "313.svg",
                "300",
                "\u96E8\u304B\u96EA\u5F8C\u66C7",
                "RAIN OR SNOW, CLOUDY LATER",
            ],
            320: [
                "311.svg",
                "711.svg",
                "300",
                "\u671D\u306E\u5185\u96E8\u5F8C\u6674",
                "RAIN IN THE MORNING, CLEAR LATER",
            ],
            321: [
                "313.svg",
                "313.svg",
                "300",
                "\u671D\u306E\u5185\u96E8\u5F8C\u66C7",
                "RAIN IN THE MORNING, CLOUDY LATER",
            ],
            322: [
                "303.svg",
                "303.svg",
                "400",
                "\u96E8\u671D\u6669\u4E00\u6642\u96EA",
                "OCCASIONAL SNOW IN THE MORNING AND EVENING, RAIN DURING THE DAY",
            ],
            323: [
                "311.svg",
                "711.svg",
                "300",
                "\u96E8\u663C\u9803\u304B\u3089\u6674",
                "RAIN, CLEAR IN THE AFTERNOON",
            ],
            324: [
                "311.svg",
                "711.svg",
                "300",
                "\u96E8\u5915\u65B9\u304B\u3089\u6674",
                "RAIN, CLEAR IN THE EVENING",
            ],
            325: [
                "311.svg",
                "711.svg",
                "300",
                "\u96E8\u591C\u306F\u6674",
                "RAIN, CLEAR IN THE NIGHT",
            ],
            326: [
                "314.svg",
                "314.svg",
                "400",
                "\u96E8\u5915\u65B9\u304B\u3089\u96EA",
                "RAIN, SNOW IN THE EVENING",
            ],
            327: [
                "314.svg",
                "314.svg",
                "400",
                "\u96E8\u591C\u306F\u96EA",
                "RAIN,SNOW IN THE NIGHT",
            ],
            328: [
                "300.svg",
                "300.svg",
                "300",
                "\u96E8\u4E00\u6642\u5F37\u304F\u964D\u308B",
                "RAIN, EXPECT OCCASIONAL HEAVY RAINFALL",
            ],
            329: [
                "300.svg",
                "300.svg",
                "300",
                "\u96E8\u4E00\u6642\u307F\u305E\u308C",
                "RAIN, OCCASIONAL SLEET",
            ],
            340: ["400.svg", "400.svg", "400", "\u96EA\u304B\u96E8", "SNOWORRAIN"],
            350: [
                "300.svg",
                "300.svg",
                "300",
                "\u96E8\u3067\u96F7\u3092\u4F34\u3046",
                "RAIN AND THUNDER",
            ],
            361: [
                "411.svg",
                "811.svg",
                "400",
                "\u96EA\u304B\u96E8\u5F8C\u6674",
                "SNOW OR RAIN, CLEAR LATER",
            ],
            371: [
                "413.svg",
                "413.svg",
                "400",
                "\u96EA\u304B\u96E8\u5F8C\u66C7",
                "SNOW OR RAIN, CLOUDY LATER",
            ],
            400: ["400.svg", "400.svg", "400", "\u96EA", "SNOW"],
            401: [
                "401.svg",
                "801.svg",
                "400",
                "\u96EA\u6642\u3005\u6674",
                "SNOW, FREQUENT CLEAR",
            ],
            402: [
                "402.svg",
                "402.svg",
                "400",
                "\u96EA\u6642\u3005\u6B62\u3080",
                "SNOWTHROUGHOUT THE DAY",
            ],
            403: [
                "403.svg",
                "403.svg",
                "400",
                "\u96EA\u6642\u3005\u96E8",
                "SNOW,FREQUENT SCCATERED SHOWERS",
            ],
            405: ["400.svg", "400.svg", "400", "\u5927\u96EA", "HEAVYSNOW"],
            406: ["406.svg", "406.svg", "400", "\u98A8\u96EA\u5F37\u3044", "SNOWSTORM"],
            407: ["406.svg", "406.svg", "400", "\u66B4\u98A8\u96EA", "HEAVYSNOWSTORM"],
            409: [
                "403.svg",
                "403.svg",
                "400",
                "\u96EA\u4E00\u6642\u96E8",
                "SNOW, OCCASIONAL SCCATERED SHOWERS",
            ],
            411: [
                "411.svg",
                "811.svg",
                "400",
                "\u96EA\u5F8C\u6674",
                "SNOW,CLEAR LATER",
            ],
            413: [
                "413.svg",
                "413.svg",
                "400",
                "\u96EA\u5F8C\u66C7",
                "SNOW,CLOUDY LATER",
            ],
            414: ["414.svg", "414.svg", "400", "\u96EA\u5F8C\u96E8", "SNOW,RAIN LATER"],
            420: [
                "411.svg",
                "811.svg",
                "400",
                "\u671D\u306E\u5185\u96EA\u5F8C\u6674",
                "SNOW IN THE MORNING, CLEAR LATER",
            ],
            421: [
                "413.svg",
                "413.svg",
                "400",
                "\u671D\u306E\u5185\u96EA\u5F8C\u66C7",
                "SNOW IN THE MORNING, CLOUDY LATER",
            ],
            422: [
                "414.svg",
                "414.svg",
                "400",
                "\u96EA\u663C\u9803\u304B\u3089\u96E8",
                "SNOW, RAIN IN THE AFTERNOON",
            ],
            423: [
                "414.svg",
                "414.svg",
                "400",
                "\u96EA\u5915\u65B9\u304B\u3089\u96E8",
                "SNOW, RAIN IN THE EVENING",
            ],
            425: [
                "400.svg",
                "400.svg",
                "400",
                "\u96EA\u4E00\u6642\u5F37\u304F\u964D\u308B",
                "SNOW, EXPECT OCCASIONAL HEAVY SNOWFALL",
            ],
            426: [
                "400.svg",
                "400.svg",
                "400",
                "\u96EA\u5F8C\u307F\u305E\u308C",
                "SNOW, SLEET LATER",
            ],
            427: [
                "400.svg",
                "400.svg",
                "400",
                "\u96EA\u4E00\u6642\u307F\u305E\u308C",
                "SNOW, OCCASIONAL SLEET",
            ],
            450: [
                "400.svg",
                "400.svg",
                "400",
                "\u96EA\u3067\u96F7\u3092\u4F34\u3046",
                "SNOW AND THUNDER",
            ],
        }

        # Download svg
        url_base = "https://www.jma.go.jp/bosai/forecast/img/"
        download_dir = os.path.expanduser("~/Downloads/images/")
        os.makedirs(download_dir, exist_ok=True)
        for code in data.keys():
            svg_name = f"{code}.svg"
            url = f"{url_base}{svg_name}"

            response = requests.get(url, stream=True)

            if response.status_code == 200:
                with open(os.path.join(download_dir, svg_name), "wb") as f:
                    response.raw.decode_content = True
                    shutil.copyfileobj(response.raw, f)
                print(f"Downloaded {url}")
            else:
                print(f"Error downloading {url}")

        # Clear
        JmaWeatherCode.objects.all().delete()

        weather_code_list = [
            JmaConstWeatherCode(key, *values) for key, values in data.items()
        ]

        jma_weather_code_list = [
            JmaWeatherCode(
                code=x.code,
                image=x.image_day,
                summary_code=x.summary_code,
                name=x.name,
                name_en=x.name_en,
            )
            for x in weather_code_list
        ]
        JmaWeatherCode.objects.bulk_create(jma_weather_code_list)

        self.stdout.write(
            self.style.SUCCESS(
                "Successfully imported all jma weather code master from manual data."
            )
        )
        
```
```console:console
jma_weather> python manage.py import_weather_master_manual
  Successfully imported all jma weather code master from manual data.
```

### create import_weather_master.py
```python:weather/management/commands/import_weather_master.py
import sys

import requests
from django.core.management.base import BaseCommand

from weather.domain.valueobject.weather.jma import JmaConstGeographicArea
from weather.models import (
    JmaArea,
    JmaRegion,
    JmaPrefecture,
    JmaCity,
    JmaAmedas,
)


def get_data_from_url(url: str):
    try:
        # Obtain the response from the URL
        response = requests.get(url)
        response.raise_for_status()
    except requests.exceptions.RequestException as e:
        print(
            f"データの取得でエラーが発生しました。URL: {url} エラー詳細: {e}",
            file=sys.stderr,
        )
        sys.exit(1)
    # Parse the JSON response into a Python dictionary
    return response.json()


class Command(BaseCommand):
    help = "Import jma const master from data."

    def handle(self, *args, **options):
        """
        Args:
            *args: Additional positional arguments (unused).
            **options: Additional keyword arguments (unused).
        """
        # Part1: from const
        url = "https://www.jma.go.jp/bosai/common/const/area.json"
        raw_data = get_data_from_url(url)

        # Create and save JmaArea: 010600 近畿地方
        JmaArea.objects.all().delete()
        jma_area_list = [
            JmaConstGeographicArea(
                code=code,
                name=data["name"],
                children=data.get("children", []),
                parent=data.get("parent"),
            )
            for code, data in raw_data["centers"].items()
        ]
        JmaArea.objects.bulk_create(
            [JmaArea(code=vo.code, name=vo.name) for vo in jma_area_list]
        )
        jma_area_cache = {obj.code: obj for obj in JmaArea.objects.all()}

        # Create and save JmaPrefecture: 280000 兵庫県
        JmaPrefecture.objects.all().delete()
        jma_prefecture_list = [
            JmaConstGeographicArea(
                code=code,
                name=data["name"],
                children=data.get("children", []),
                parent=data.get("parent"),
            )
            for code, data in raw_data["offices"].items()
        ]
        JmaPrefecture.objects.bulk_create(
            [
                JmaPrefecture(
                    code=vo.code, name=vo.name, jma_area=jma_area_cache.get(vo.parent)
                )
                for vo in jma_prefecture_list
            ]
        )
        jma_prefecture_cache = {obj.code: obj for obj in JmaPrefecture.objects.all()}

        # Create and save JmaRegion: 280010 南部
        JmaRegion.objects.all().delete()
        jma_region_list = [
            JmaConstGeographicArea(
                code=code,
                name=data["name"],
                children=data.get("children", []),
                parent=data.get("parent"),
            )
            for code, data in raw_data["class10s"].items()
        ]
        JmaRegion.objects.bulk_create(
            [
                JmaRegion(
                    code=vo.code,
                    name=vo.name,
                    jma_prefecture=jma_prefecture_cache.get(vo.parent),
                )
                for vo in jma_region_list
            ]
        )
        jma_region_cache = {obj.code: obj for obj in JmaRegion.objects.all()}

        # Create JmaCityGroup
        jma_city_group_list = [
            JmaConstGeographicArea(
                code=code,
                name=data["name"],
                children=data.get("children", []),
                parent=data.get("parent"),
            )
            for code, data in raw_data["class15s"].items()
        ]
        jma_city_group_cache = {
            jma_city_group.code: jma_city_group
            for jma_city_group in jma_city_group_list
        }

        # Create and save JmaCity with parents via JmaCityGroup: 2820100 姫路市
        JmaCity.objects.all().delete()
        jma_city_list = [
            JmaConstGeographicArea(
                code=code,
                name=data["name"],
                children=data.get("children", []),
                parent=data.get("parent"),
            )
            for code, data in raw_data["class20s"].items()
        ]
        JmaCity.objects.bulk_create(
            [
                JmaCity(
                    code=vo.code,
                    name=vo.name,
                    jma_region=jma_region_cache.get(
                        jma_city_group_cache.get(vo.parent).parent
                    ),
                )
                for vo in jma_city_list
            ]
        )

        # Part2: from forecast_area.json
        url = "https://www.jma.go.jp/bosai/forecast/const/forecast_area.json"
        raw_data = get_data_from_url(url)

        JmaAmedas.objects.all().delete()
        jma_amedas_list = []
        for prefecture_code, entries in raw_data.items():
            for entry in entries:
                region_code = entry["class10"]
                for amedas_code in entry["amedas"]:
                    jma_amedas_list.append(
                        JmaAmedas(
                            code=amedas_code,
                            jma_region=jma_region_cache.get(region_code),
                        )
                    )
        JmaAmedas.objects.bulk_create(jma_amedas_list)

        self.stdout.write(
            self.style.SUCCESS("jma const master data import has been completed.")
        )

```
```console:console
jma_weather> python manage.py import_weather_master
  jma const master data import has been completed.
```

## 予報と警報を保存する仕組みを作ろう
### 警報の名称
https://doc01.pf.iij-engineering.co.jp/pub/sdkdoc/v1/ja_JP/restapi/rest_jmaweather_ver1.html

### 複数の日の天気情報が混ざってる
これを取り出すの頭使ったな...データ構造としてひねくれとる
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/0038b6f2-1657-955b-2541-d86ca8485b40.png">

### regionの気温はアメダスの平均
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/ad456642-40d6-08fa-2f83-0aa371e0a624.png)

### ※（鬼畜の仕様）十勝地方と奄美地方は天気の取得が失敗する！
たまたま台風の注意報を確認したくて奄美地方の建物作って天気を取得してみたら処理が事故った。調査してみると代理のprefectureに天気予報が収録されている。area, prefecture, region, city の関係をしっかりと捉えていたから事故ったときにピンときてソースを見るというアタリがついた。こ、これがドキュメント化されていないというのは鬼畜の所業だぜ :japanese_ogre:
（天気予報のページからソースを開いて...）
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/b743ee78-bba2-dc09-6b1e-f186a7aa8ab0.png">
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94562/7c226337-8c2b-43cb-1361-82205b243ede.png">

### create fetch_weather_forecast.py
```python:weather/management/commands/fetch_weather_forecast.py
from datetime import datetime, date

import requests
from django.core.management import BaseCommand

from weather.domain.repository.weather.jma import JmaRepository
from weather.domain.valueobject.weather.jma import (
    WindData,
    MeanCalculable,
    Region,
    SummaryText,
    RainData,
    TemperatureData,
)
from weather.models import (
    JmaWeather,
    JmaRegion,
    JmaWeatherCode,
)

THREE_DAYS = 0

TYPE_OVERVIEW = 0
TYPE_RAIN = 1
TYPE_TEMPERATURE = 2
TYPE_WIND = 1

WIND_SPEED = 3
LAND = 0


def update_prefecture_ids(prefecture_ids: list[str]) -> tuple[list[str], dict]:
    """
    気象庁特別ルール
    "014030" があって "014100" がないときに "014030" → "014100" に置換 / 十勝地方 → 釧路・根室地方
    "460040" があって "460100" がないときに "460040" → "460100" に置換 / 奄美地方 → 鹿児島県（奄美地方除く）

    Args:
        prefecture_ids (list[str]): The list of JMA prefecture ids to be updated.

    Returns:
        tuple[list[str], dict]: JMA prefecture ids and special add region ids.

    Example:
        prefecture_ids = ["014030", "460040"]
        updated_ids, special_add_region_ids = update_prefecture_ids(prefecture_ids)
        print(updated_ids)  # Output: ["014100", "460100"]
        print(special_add_region_ids)  # Output: {"014100": "014030", "460100": "460040"}
    """
    pairs_to_check = [("014030", "014100"), ("460040", "460100")]
    special_add_region_ids = {}

    for invalid_id, proxy_id in pairs_to_check:
        if invalid_id in prefecture_ids:
            if proxy_id not in prefecture_ids:
                prefecture_ids.append(proxy_id)
            prefecture_ids.remove(invalid_id)
            special_add_region_ids[proxy_id] = invalid_id
    return prefecture_ids, special_add_region_ids


def get_data(url: str):
    response = requests.get(url)
    response.raise_for_status()
    data = response.json()
    return data[THREE_DAYS]["timeSeries"]


def get_indexes(data_time_defines, desired_date: date) -> list[int]:
    """
    get_indexes する箇所が複数あるので独立さした

    Args:
        data_time_defines: ["2024-08-31", "2024-09-01", "2024-09-01"]
        desired_date: "2024-09-01"

    Returns: [1, 2]
    """
    return [
        i
        for i, date_str in enumerate(data_time_defines)
        if datetime.fromisoformat(date_str).date() == desired_date
    ]


class Command(BaseCommand):
    help = "get weather forecast"

    def handle(self, *args, **options):
        jma_weather_list: list[JmaWeather] = []

        # TODO: 例えば建物の jma_prefecture_ids をdbから取得（重複を削って）
        jma_prefecture_ids = ["280000", "050000", "130000", "014030", "460040"]
        jma_prefecture_ids, special_add_region_ids = update_prefecture_ids(
            jma_prefecture_ids
        )

        if not jma_prefecture_ids:
            raise Exception("facility is empty")

        region_master = {x.code: x for x in JmaRegion.objects.all()}
        weather_code_master = {x.code: x for x in JmaWeatherCode.objects.all()}

        # 日付のリストだけ取得
        url = f"https://www.jma.go.jp/bosai/forecast/data/forecast/{jma_prefecture_ids[0]}.json"
        time_series_data = get_data(url)
        target_date_list = [
            datetime.fromisoformat(date_str).date()
            for date_str in time_series_data[TYPE_OVERVIEW]["timeDefines"]
        ]

        JmaWeather.objects.all().delete()
        for target_date in target_date_list:
            for prefecture_id in jma_prefecture_ids:
                print(f"{target_date} の {prefecture_id=}")
                forecasts_by_region = {}

                print("  風速:")
                url = f"https://www.jma.go.jp/bosai/probability/data/probability/{prefecture_id}.json"
                time_series_data = get_data(url)

                indexes = get_indexes(
                    data_time_defines=time_series_data[TYPE_WIND]["timeDefines"],
                    desired_date=target_date,
                )
                for region_data in time_series_data[TYPE_WIND]["areas"]:
                    region_code = region_data["code"]
                    forecasts_by_region.setdefault(region_code, {})
                    time_cells_wind_data = region_data["properties"][WIND_SPEED][
                        "timeCells"
                    ]
                    wind_data = WindData(
                        values=MeanCalculable(
                            [
                                int(time_cell["locals"][LAND]["value"])
                                for i, time_cell in enumerate(time_cells_wind_data)
                                if i in indexes
                            ]
                        )
                    )
                    forecasts_by_region[region_code]["wind_data"] = wind_data
                    print(f"    {region_code} の {wind_data}")

                print("  天気サマリ:")
                url = f"https://www.jma.go.jp/bosai/forecast/data/forecast/{prefecture_id}.json"
                time_series_data = get_data(url)

                indexes = get_indexes(
                    data_time_defines=time_series_data[TYPE_OVERVIEW]["timeDefines"],
                    desired_date=target_date,
                )
                if len(indexes) != 1:
                    print(f"    要素数は必ず1になります{len(indexes)}")
                    continue
                index = indexes.pop()
                for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
                    region = Region(
                        code=region_data["area"]["code"],
                        name=region_data["area"]["name"],
                    )
                    forecasts_by_region.setdefault(region.code, {})

                    # weather_code
                    forecasts_by_region[region.code]["weather_code"] = region_data[
                        "weatherCodes"
                    ][index]

                    # summary_text を3種
                    summary_text = SummaryText(
                        weather=(
                            region_data["weathers"][index]
                            if "weathers" in region_data
                            and index < len(region_data["weathers"])
                            else "なし"
                        ),
                        wind=(
                            region_data["winds"][index]
                            if "winds" in region_data
                            and index < len(region_data["winds"])
                            else "なし"
                        ),
                        wave=(
                            region_data["waves"][index]
                            if "waves" in region_data
                            and index < len(region_data["waves"])
                            else "なし"
                        ),
                    )
                    forecasts_by_region[region.code]["summary_text"] = summary_text
                    summary_text_merge = f"{summary_text.weather[:4]}｜{summary_text.wind[:4]}｜{summary_text.wave[:4]}｜"
                    print(
                        f"    {region.code} は ｜{region_data['weatherCodes'][index]}｜{summary_text_merge}"
                    )

                print("  降水確率:")
                indexes = get_indexes(
                    data_time_defines=time_series_data[TYPE_RAIN]["timeDefines"],
                    desired_date=target_date,
                )
                for region_data in time_series_data[TYPE_RAIN]["areas"]:
                    region = Region(
                        code=region_data["area"]["code"],
                        name=region_data["area"]["name"],
                    )
                    forecasts_by_region.setdefault(region.code, {})
                    rain_data = RainData(
                        values=MeanCalculable(
                            [
                                int(time_cell)
                                for i, time_cell in enumerate(region_data["pops"])
                                if i in indexes
                            ]
                        )
                    )
                    forecasts_by_region[region.code]["rain_data"] = rain_data
                    print(
                        f"    {region.code} の {rain_data.values.mean} {rain_data.unit}"
                    )

                print("  気温:")
                amedas_code_in_region = JmaRepository.get_amedas_code_list(
                    prefecture_id, special_add_region_ids
                )
                indexes = get_indexes(
                    data_time_defines=time_series_data[TYPE_TEMPERATURE]["timeDefines"],
                    desired_date=target_date,
                )
                for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
                    region = Region(
                        code=region_data["area"]["code"],
                        name=region_data["area"]["name"],
                    )
                    forecasts_by_region.setdefault(region.code, {})

                    amedas_min_temps: list[float] = []
                    amedas_max_temps: list[float] = []
                    if indexes:
                        min_index, max_index = indexes
                        for amedas_data in time_series_data[TYPE_TEMPERATURE]["areas"]:
                            amedas_code = amedas_data["area"]["code"]
                            if amedas_code not in amedas_code_in_region.get(
                                region.code
                            ):
                                continue
                            amedas_min_temps.append(
                                float(amedas_data["temps"][min_index])
                            )
                            amedas_max_temps.append(
                                float(amedas_data["temps"][max_index])
                            )
                    temperature_data = TemperatureData(
                        min_values=MeanCalculable(amedas_min_temps),
                        max_values=MeanCalculable(amedas_max_temps),
                    )
                    forecasts_by_region[region.code][
                        "temperature_data"
                    ] = temperature_data
                    msg1 = f"    {region.code} の最低気温 {temperature_data.min_values.mean} {temperature_data.unit}"
                    msg2 = f"最高気温 {temperature_data.max_values.mean} {temperature_data.unit}"
                    print(f"{msg1} / {msg2}")

                # 天気・風・波・降水確率・気温 を1日分としてガッチャンコ
                for region_data in time_series_data[TYPE_OVERVIEW]["areas"]:
                    region = Region(
                        code=region_data["area"]["code"],
                        name=region_data["area"]["name"],
                    )

                    weather_code = forecasts_by_region[region.code]["weather_code"]
                    summary_text = forecasts_by_region[region.code]["summary_text"]
                    rain_data = forecasts_by_region[region.code]["rain_data"]
                    temperature_data = forecasts_by_region[region.code][
                        "temperature_data"
                    ]
                    wind_data = forecasts_by_region[region.code]["wind_data"]
                    jma_weather_list.append(
                        JmaWeather(
                            jma_region=region_master.get(region.code),
                            reporting_date=target_date,
                            jma_weather_code=weather_code_master.get(weather_code),
                            weather_text=summary_text.weather,
                            wind_text=summary_text.wind,
                            wave_text=summary_text.wave,
                            avg_rain_probability=(
                                None
                                if len(rain_data.values.raw) == 0
                                else rain_data.values.mean
                            ),
                            avg_min_temperature=(
                                None
                                if len(temperature_data.min_values.raw) == 0
                                else temperature_data.min_values.mean
                            ),
                            avg_max_temperature=(
                                None
                                if len(temperature_data.max_values.raw) == 0
                                else temperature_data.max_values.mean
                            ),
                            avg_max_wind_speed=(
                                None
                                if len(wind_data.values.raw) == 0
                                else wind_data.values.mean
                            ),
                        )
                    )
        JmaWeather.objects.bulk_create(jma_weather_list)

        self.stdout.write(
            self.style.SUCCESS("weather forecast data retrieve has been completed.")
        )

```
```console:console
dev\portfolio> python manage.py fetch_weather_forecast
2024-09-01 の prefecture_id='280000'
  風速:
    280010 の 最大風速（時間帯平均）は 9.0 以下
    280020 の 最大風速（時間帯平均）は 9.0 以下
  天気サマリ:
    280010 は ｜200｜くもり　｜北西の風｜１メート｜
    280020 は ｜200｜くもり　｜北の風　｜１．５メ｜
  降水確率:
    280010 の 40.0 %
    280020 の 40.0 %
  気温:
    280010 の最低気温 0 ℃ / 最高気温 0 ℃
    280020 の最低気温 0 ℃ / 最高気温 0 ℃

2024-09-02 の prefecture_id='280000'
  風速:
    280010 の 最大風速（時間帯平均）は 9.0 以下
    280020 の 最大風速（時間帯平均）は 9.0 以下
  天気サマリ:
    280010 は ｜313｜雨　昼前｜南西の風｜１メート｜
    280020 は ｜203｜くもり　｜南の風　｜１メート｜
  降水確率:
    280010 の 40.0 %
    280020 の 50.0 %
  気温:
    280010 の最低気温 24.3 ℃ / 最高気温 33.0 ℃
    280020 の最低気温 24.0 ℃ / 最高気温 33.0 ℃

2024-09-03 の prefecture_id='280000'
  風速:
    280010 の 最大風速（時間帯平均）は 0 以下
    280020 の 最大風速（時間帯平均）は 0 以下
  天気サマリ:
    280010 は ｜201｜くもり　｜北の風　｜０．５メ｜
    280020 は ｜201｜くもり　｜北の風｜１メート｜
  降水確率:
    280010 の 0 %
    280020 の 0 %
  気温:
    280010 の最低気温 0 ℃ / 最高気温 0 ℃
    280020 の最低気温 0 ℃ / 最高気温 0 ℃
    
```

### create fetch_weather_warning.py
```python:weather/management/commands/fetch_weather_warning.py
import requests
from django.core.management import BaseCommand

from weather.domain.valueobject.weather.jma import WarningData
from weather.models import JmaWarning, JmaRegion

LATEST_WARNING = 0
M_TARGET_WARNINGS = {
    "02": "暴風雪警報",
    "03": "大雨警報",
    "04": "洪水警報",
    "05": "暴風警報",
    "06": "大雪警報",
    "07": "波浪警報",
    "08": "高潮警報",
    "10": "大雨注意報",
    "12": "大雪注意報",
    "13": "風雪注意報",
    "14": "雷注意報",
    "15": "強風注意報",
    "16": "波浪注意報",
    "17": "融雪注意報",
    "18": "洪水注意報",
    "19": "高潮注意報",
    "20": "濃霧注意報",
    "21": "乾燥注意報",
    "22": "なだれ注意報",
    "23": "低温注意報",
    "24": "霜注意報",
    "25": "着氷注意報",
    "26": "着雪注意報",
    "27": "その他の注意報",
    "32": "暴風雪特別警報",
    "33": "大雨特別警報",
    "35": "暴風特別警報",
    "36": "大雪特別警報",
    "37": "波浪特別警報",
    "38": "高潮特別警報",
}


def get_data(url: str):
    response = requests.get(url)
    response.raise_for_status()
    return response.json()


class Command(BaseCommand):
    help = "get weather warning"

    def handle(self, *args, **options):
        # TODO: facilityテーブルから prefecture_ids を取得
        jma_prefecture_ids = ["280000", "050000", "130000", "014030", "460040"]

        if not jma_prefecture_ids:
            raise Exception("facility is empty")

        region_master = {x.code: x for x in JmaRegion.objects.all()}

        JmaWarning.objects.all().delete()
        jma_warning_list: list[JmaWarning] = []
        for prefecture_id in jma_prefecture_ids:
            url = (
                f"https://www.jma.go.jp/bosai/warning/data/warning/{prefecture_id}.json"
            )
            latest_warning_data = get_data(url)["areaTypes"][LATEST_WARNING]

            for region_data in latest_warning_data["areas"]:
                region_code = region_data["code"]

                warning_data_list = [
                    WarningData(code=x["code"], status=x["status"])
                    for x in region_data["warnings"]
                    if "code" in x
                ]

                if not warning_data_list:
                    continue

                warnings: list[str] = list(
                    set(
                        [
                            M_TARGET_WARNINGS[warning_data.code]
                            for warning_data in warning_data_list
                        ]
                    )
                )
                jma_warning_list.append(
                    JmaWarning(
                        jma_region=region_master.get(region_code),
                        warnings=",".join(warnings),
                    )
                )
        JmaWarning.objects.bulk_create(jma_warning_list)

        self.stdout.write(
            self.style.SUCCESS("weather warning data retrieve has been completed.")
        )

```
```console:console
jma_weather> python manage.py fetch_weather_warning 
  280000/280010 の 雷注意報
  280000/280020 の 雷注意報, 高潮注意報
  050000/050010 の 雷注意報
  050000/050020 の 雷注意報
  130000/130010 の 大雨警報, 雷注意報
  130000/130020 の 雷注意報, 波浪注意報
  130000/130030 の 雷注意報, 波浪注意報
  weather warning data retrieve has been completed.
```

## さいごに
まぁリージョンごとに予報データをしまうことができたのであとはタテモノと紐づけるなりしてよりよいものを作ることはできるだろう。予報と警報はひとつのバッチでもいいかもしれない。仕事では、天気は日次バッチ、警報はリアルタイムという要件だったので割れただけだ

https://github.com/duri0214/portfolio/tree/master/soil_analysis

## Next Action
https://qiita.com/YoshitakaOkada/items/5cc23a68991460c7632b