> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Building a project from scratch

> Guide to building a minimal Flask METAR reader that fetches METARs from aviationweather.gov and decodes raw reports into human friendly wind, visibility, clouds, temperature and altimeter summaries.

In this lesson/article we scaffold a minimal METAR reader web app using Flask and Python. The app accepts an airport ICAO code (for example, KJFK or KHIO), fetches the current METAR from aviationweather.gov (ADDS), and presents both the raw METAR string and a human-friendly decoded weather summary (wind, visibility, clouds, temperature/dew point, and altimeter).

This guide gives a compact, working example you can run locally, extend, and test. Keywords: METAR reader, Flask app, METAR decoder, aviationweather, Python, requests.

What is a METAR?

* METAR is a standardized, concise aviation weather observation string that contains station identifier, time, wind, visibility, cloud layers, temperature/dew point, altimeter, and optional remark fields. METARs are machine- and human-readable for trained users but can look cryptic to the general public.

Example raw METARs:

```bash theme={null}
KJFK 052151Z 16008KT 10SM SCT047 SCT210 BKN250 24/21 A3031 RMK AO2 SLP262 T02440206 $
```

```bash theme={null}
KHIO 052253Z 00000KT 10SM CLR 27/11 A3005 RMK AO2 SLP172 T02670106
```

A pilot or meteorologist can parse those tokens to extract wind, visibility, clouds, temperature, pressure and more. Our app focuses on fetching these strings and converting common tokens to plain English (for example: "Clear day, 81°F (27°C), calm wind").

Quick project goals

* Build a Flask web application
* Provide an HTML form to input an airport ICAO code
* Query the ADDS (aviationweather.gov) METAR endpoint for the most recent METAR
* Decode common METAR tokens (wind, visibility, clouds, temperature/dew point, pressure/altimeter)
* Display both the decoded summary and the raw METAR
* Add basic error handling and keep the project minimal and extensible

Project structure (files)

| File / Resource | Purpose |
| - | - |
| `app.py` | Flask application and METAR decoder logic |
| `templates/index.html` | Input form to enter airport code |
| `templates/result.html` | Displays decoded and raw METAR |
| `static/style.css` | Simple styling |
| `requirements.txt` | Python dependencies |

Security warning when using Claude Code locally

<Callout icon="warning" color="#FF6B6B">
  The CLI snippet below shows a prompt from Claude Code asking whether to trust files in a folder. Only allow execution of code from sources you trust. Executing untrusted code can be unsafe.
</Callout>

Starting Claude Code for Beginners in the repository (example CLI interaction)

```bash theme={null}
jeremy@MACSTUDIO KodeKloud-METAR-Reader %
claude

Do you trust the files in this folder?
/Users/jeremy/Repos/KodeKloud-METAR-Reader

Claude Code may read files in this folder. Reading untrusted files may lead Claude Code to behave in unexpected ways.

With your permission Claude Code may execute files in this folder. Executing untrusted code is unsafe.

https://docs.anthropic.com/s/claude-code-security

> 1. Yes, proceed
2. No, exit

Enter to confirm · Esc to exit
```

High-level to-dos that guided the scaffolding:

* Set up a Flask application structure with a main `app.py` file
* Create an HTML template for an airport code input form
* Implement METAR fetching from aviationweather.gov (ADDS) API
* Create a METAR decoder that converts cryptic tokens to readable text
* Add error handling for invalid airport codes and network errors
* Create a results template that shows both the decoded report and raw METAR
* Add minimal CSS styling for readability
* Test the application with several airport codes

A compact, working example

Below is a concise, functional implementation you can copy, run, and extend. Keep the code blocks as separate files in your project.

app.py

```python theme={null}
# app.py
from flask import Flask, render_template, request
import requests
import re
import csv
from datetime import datetime

app = Flask(__name__)

class METARDecoder:
    def __init__(self):
        self.wind_directions = {
            'N': 'north', 'NNE': 'north-northeast', 'NE': 'northeast',
            'ENE': 'east-northeast', 'E': 'east', 'ESE': 'east-southeast',
            'SE': 'southeast', 'SSE': 'south-southeast', 'S': 'south',
            'SSW': 'south-southwest', 'SW': 'southwest', 'WSW': 'west-southwest',
            'W': 'west', 'WNW': 'west-northwest', 'NW': 'northwest',
            'NNW': 'north-northwest'
        }

    def decode_wind(self, wind_token: str):
        # Examples: 16008KT, 00000KT, VRB03KT, 18012G20KT
        if not wind_token:
            return "Wind: not reported"
        if wind_token.startswith("VRB"):
            m = re.match(r'VRB(\d{2,3})KT', wind_token)
            if m:
                return f"Variable wind at {int(m.group(1))} knots"
        if wind_token == '00000KT':
            return "Calm (0 knots)"
        m = re.match(r'(\d{3})(\d{2,3})(G(\d{2,3}))?KT', wind_token)
        if m:
            deg = int(m.group(1))
            speed = int(m.group(2))
            gust = m.group(4)
            # convert degrees to compass abbreviation (16 sectors)
            dirs = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
                    "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
            idx = int((deg + 11.25) // 22.5) % 16
            dir_text = self.wind_directions.get(dirs[idx], dirs[idx])
            gust_text = f", gusts to {gust} knots" if gust else ""
            return f"Wind: {speed} knots from the {dir_text} ({deg}°){gust_text}"
        return f"Wind: {wind_token}"

    def decode_temp_dew(self, tempdew: str):
        # Example: 27/11  (Celsius)
        m = re.match(r'([M]?\d{1,2})/([M]?\d{1,2})', tempdew)
        if not m:
            return "Temperature/dewpoint not reported"
        def parse_value(s):
            if s.startswith('M'):
                return -int(s[1:])
            return int(s)
        temp_c = parse_value(m.group(1))
        dew_c = parse_value(m.group(2))
        temp_f = round((temp_c * 9/5) + 32)
        dew_f = round((dew_c * 9/5) + 32)
        return f"Temperature: {temp_f}°F ({temp_c}°C), Dew point: {dew_f}°F ({dew_c}°C)"

    def decode_visibility(self, vis_token: str):
        # Example: 10SM (10 statute miles)
        if not vis_token:
            return "Visibility: not reported"
        m = re.match(r'(\d+)SM', vis_token)
        if m:
            return f"Visibility: {m.group(1)} statute miles"
        return f"Visibility: {vis_token}"

    def decode_clouds(self, clouds_section: str):
        # Simplified handling: look for CLR, FEW, SCT, BKN, OVC and heights (in hundreds of feet)
        if not clouds_section or clouds_section.strip() == '':
            return "Sky: not reported"
        if 'CLR' in clouds_section or 'SKC' in clouds_section:
            return "Clear skies"
        clouds = []
        tokens = clouds_section.split()
        for tok in tokens:
            m = re.match(r'(FEW|SCT|BKN|OVC)(\d{3})', tok)
            if m:
                amount = m.group(1)
                height_hundreds = int(m.group(2))
                height_ft = height_hundreds * 100
                desc = {
                    'FEW': 'a few clouds',
                    'SCT': 'scattered clouds',
                    'BKN': 'broken clouds',
                    'OVC': 'overcast'
                }.get(amount, amount)
                clouds.append(f"{desc} at {height_ft} ft")
        return "Sky: " + (", ".join(clouds) if clouds else clouds_section)

    def decode_altimeter(self, alt_token: str):
        # Example: A3005 -> 30.05 inHg
        m = re.match(r'A(\d{4})', alt_token)
        if m:
            inches = float(m.group(1)) / 100.0
            return f"Altimeter: {inches:.2f} inHg"
        return "Altimeter: not reported"

    def decode_metar(self, metar_string: str):
        # Very simple parser, intended for common, simple METARs.
        # Split tokens and attempt to find standard fields.
        tokens = metar_string.split()
        decoded = {}
        # Station
        if tokens:
            decoded['station'] = tokens[0]
        # Time token (e.g., 052253Z)
        for tok in tokens[1:4]:
            if tok.endswith('Z') and re.match(r'\d{6}Z', tok):
                decoded['time'] = tok
                break
        # Find wind, visibility, temp/dew, altimeter
        for tok in tokens:
            if re.match(r'(\d{3}|VRB)\d{2,3}(G\d{2,3})?KT', tok) or tok == '00000KT':
                decoded['wind'] = self.decode_wind(tok)
            elif tok.endswith('SM'):
                decoded['visibility'] = self.decode_visibility(tok)
            elif re.match(r'[M]?\d{1,2}/[M]?\d{1,2}', tok):
                decoded['temp_dew'] = self.decode_temp_dew(tok)
            elif tok.startswith('A') and re.match(r'A\d{4}', tok):
                decoded['altimeter'] = self.decode_altimeter(tok)
            elif tok in ['CLR', 'SKC'] or re.match(r'(FEW|SCT|BKN|OVC)\d{3}', tok):
                # Gather the rest of the cloud tokens starting from here
                idx = tokens.index(tok)
                cloud_section = ' '.join(tokens[idx:idx+4])  # small window
                decoded['clouds'] = self.decode_clouds(cloud_section)
        # Raw METAR fallback fields
        decoded['raw'] = metar_string
        return decoded

decoder = METARDecoder()

def fetch_metar(airport_code: str):
    """
    Fetch raw METAR data for the given airport code.

    This example uses the [AviationWeather ADDS (Aviation Digital Data Service)](https://aviationweather.gov/dataserver)
    "dataserver_current" endpoint to request METARs in CSV format and extract
    the raw_text field. This is more reliable than attempting to fetch a single
    raw line from a generic HTML endpoint.

    Note: the ADDS server supports multiple parameters; here we request the most
    recent METAR for the station within the past couple of hours. For production
    use, review the ADDS API documentation and consider caching, rate limits,
    and error handling improvements.
    """
    base_url = "https://aviationweather.gov/adds/dataserver_current/httpparam"
    params = {
        'dataSource': 'metars',
        'requestType': 'retrieve',
        'format': 'csv',
        'stationString': airport_code.upper(),
        'hoursBeforeNow': '2',
        'mostRecent': 'true'
    }
    try:
        resp = requests.get(base_url, params=params, timeout=10)
        resp.raise_for_status()
        text = resp.text.strip()
        if not text:
            return None
        # Parse CSV and look for raw_text column
        try:
            reader = csv.DictReader(text.splitlines())
            for row in reader:
                raw = row.get('raw_text') or row.get('rawMETAR') or None
                if raw:
                    return raw.strip()
        except Exception:
            # Fallback: attempt to find a line that starts with the station ID
            for line in text.splitlines():
                if line.strip().upper().startswith(airport_code.upper()):
                    return line.strip()
        # Final fallback: first non-empty line
        for line in text.splitlines():
            if line.strip():
                return line.strip()
        return None
    except requests.RequestException:
        return None

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/result', methods=['POST'])
def result():
    airport = request.form.get('airport', '').strip().upper()
    if not airport:
        return render_template('result.html', error="No airport code provided.")
    raw = fetch_metar(airport)
    if not raw:
        return render_template('result.html', error=f"Could not fetch METAR for {airport}.")
    decoded = decoder.decode_metar(raw)
    return render_template('result.html', airport_code=airport, raw_metar=raw, decoded=decoded)

if __name__ == '__main__':
    app.run(debug=True)
```

templates/index.html

```html theme={null}
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>METAR Weather Reader</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <div class="container">
    <h1>METAR Weather Reader</h1>
    <p>Enter an airport code (e.g., KHIO, KJFK, KLAX) to get a friendly weather report.</p>
    <form action="{{ url_for('result') }}" method="post">
      <input type="text" name="airport" placeholder="Airport code (e.g., KHIO)" required>
      <button type="submit">Get METAR</button>
    </form>
  </div>
</body>
</html>
```

templates/result.html

```html theme={null}
<!-- templates/result.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>METAR Result - {{ airport_code }}</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <div class="container">
    {% if error %}
      <h2>Error</h2>
      <p>{{ error }}</p>
      <a href="{{ url_for('index') }}">Try another airport</a>
    {% else %}
      <h1>Weather Report for {{ airport_code }}</h1>
      <h3>Decoded</h3>
      <ul>
        {% for key, val in decoded.items() if key != 'raw' %}
          <li><strong>{{ key.replace('_', ' ').title() }}:</strong> {{ val }}</li>
        {% endfor %}
      </ul>

      <h3>Raw METAR</h3>
      <pre>{{ raw_metar }}</pre>

      <a href="{{ url_for('index') }}">Check another airport</a>
    {% endif %}
  </div>
</body>
</html>
```

static/style.css (very basic)

```css theme={null}
/* static/style.css */
body { font-family: Arial, sans-serif; background:#f5f6fb; color:#222; margin:0; padding:20px; }
.container { max-width:700px; margin:40px auto; padding:20px; background:#fff; border-radius:8px; box-shadow:0 6px 20px rgba(0,0,0,0.06); }
input[type="text"] { padding:8px 10px; width:60%; margin-right:8px; }
button { padding:8px 12px; }
pre { background:#f0f0f0; padding:10px; border-radius:4px; overflow:auto; }
```

requirements.txt

```text theme={null}
Flask==2.3.3
requests==2.31.0
```

Run locally

```bash theme={null}
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.py
```

Then open [http://127.0.0.1:5000](http://127.0.0.1:5000) in your browser.

Example: KHIO result

* When you test with `KHIO`, the app will query ADDS, retrieve the most recent METAR, and decode it. The decoded output typically shows clear skies, temperature in °F and °C, wind (calm or direction/speed), visibility, and the altimeter, plus the raw METAR string for reference.

<Frame>
  <img src="https://mintcdn.com/kodekloud-c4ac6d9a/Xs8Ruv7b0QCZf7iM/images/Claude-Code-for-Beginners/Getting-Started-with-Claude-Code/Building-a-project-from-scratch/khio-weather-report-clear-81f.jpg?fit=max&auto=format&n=Xs8Ruv7b0QCZf7iM&q=85&s=609d0e53b07110538277088c8c0d2046" alt="A webpage screenshot displaying a &#x22;Weather Report for KHIO&#x22; with current conditions: clear skies, 81°F (27°C), wind from the north at 0 knots. The page also shows detailed observations like visibility, dew point and barometric pressure." width="1920" height="1080" data-path="images/Claude-Code-for-Beginners/Getting-Started-with-Claude-Code/Building-a-project-from-scratch/khio-weather-report-clear-81f.jpg" />
</Frame>

Notes and next steps

<Callout icon="lightbulb" color="#1CB2FE">
  This example is a pragmatic starting point. The METAR decoder is intentionally simple—METARs can include runway visual range, precipitation and obscuration groups (RA, SN, TS, FG, BR), automated sensor flags (AO1/AO2), and many remark fields. For production usage, extend parsing to cover more tokens, add unit tests, rate-limit handling, caching, and improved UI/UX for mobile devices.
</Callout>

Suggested improvements and checklist

* Extend the decoder to handle precipitation, obscurations, and thunderstorm notations (e.g., `RA`, `SN`, `TS`, `FG`, `BR`).
* Add unit tests for `METARDecoder.decode_*` functions and for `fetch_metar`.
* Implement caching and exponential backoff to handle ADDS rate limits.
* Improve error messages for invalid ICAO codes and no-data responses.
* Consider requesting `format=xml` or `format=xml&dataSource=metars` from ADDS if you prefer structured parsing, or use a dedicated METAR parsing library.

Links and references

* [AviationWeather ADDS dataserver\_docs (ADDS)](https://aviationweather.gov/dataserver)
* [Flask Documentation](https://flask.palletsprojects.com/)
* [Requests — HTTP for Humans (requests)](https://docs.python-requests.org/)
* [Claude Code for Beginners course (KodeKloud)](https://learn.kodekloud.com/user/courses/claude-code-for-beginners)

This base project gives a quick path from idea to working web app so you can iterate, add documentation, tests, and integrations while keeping the core functionality clear and extensible.

<CardGroup>
  <Card title="Watch Video" icon="video" cta="Learn more" href="https://learn.kodekloud.com/user/courses/claude-code-for-beginners/module/e36fd287-dee2-4916-a919-953391788143/lesson/aa659d6b-8e5e-422c-9197-cb670928e287" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.