{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Python — Functions, APIs & Async\n",
    "\n",
    "Third notebook in the series. Builds on `python_basics.ipynb` and `python_next_steps.ipynb`.\n",
    "\n",
    "**Sections**\n",
    "1. Functions beyond the basics — defaults, `*args`, `**kwargs`, lambdas, higher-order, type hints, decorators\n",
    "2. Calling HTTP APIs — `requests`, JSON, status codes, error handling\n",
    "3. Async — `async def` / `await`, `asyncio.gather`, concurrency that actually pays off\n",
    "4. Mini exercises\n",
    "\n",
    "**Requirements:** Sections 2 needs `pip install requests` and an internet connection. Section 3 is pure standard library."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Functions beyond the basics\n",
    "\n",
    "You already know `def name(args): ...` and `return`. Here's the rest."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1a. Default arguments\n",
    "\n",
    "Give a parameter a fallback value so the caller can skip it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def greet(name, greeting=\"Sawadee\"):\n",
    "    return f\"{greeting}, {name}!\"\n",
    "\n",
    "print(greet(\"Ploy\"))\n",
    "print(greet(\"Ploy\", greeting=\"Hello\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Trap:** never use a *mutable* default (a list, dict, set). It's shared between calls and quietly accumulates state. Use `None` as the sentinel instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# WRONG - the same list is reused across calls\n",
    "def add_item_bad(item, basket=[]):\n",
    "    basket.append(item)\n",
    "    return basket\n",
    "\n",
    "print(add_item_bad(\"mango\"))\n",
    "print(add_item_bad(\"durian\"))   # surprise: contains mango too!\n",
    "\n",
    "# RIGHT\n",
    "def add_item(item, basket=None):\n",
    "    if basket is None:\n",
    "        basket = []\n",
    "    basket.append(item)\n",
    "    return basket\n",
    "\n",
    "print(add_item(\"mango\"))\n",
    "print(add_item(\"durian\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1b. `*args` and `**kwargs`\n",
    "\n",
    "Accept any number of positional or keyword arguments. The names `args` and `kwargs` are just convention — the `*` and `**` are what matter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def total(*numbers):\n",
    "    return sum(numbers)\n",
    "\n",
    "print(total(1, 2, 3))\n",
    "print(total(10, 20, 30, 40, 50))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def describe(**fields):\n",
    "    for key, value in fields.items():\n",
    "        print(f\"  {key}: {value}\")\n",
    "\n",
    "describe(name=\"Ploy\", age=12, city=\"Chiang Mai\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1c. Lambda — small inline functions\n",
    "\n",
    "A `lambda` is a tiny anonymous function. Use it where you'd otherwise have to define a one-line function just to pass it somewhere."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "double = lambda n: n * 2\n",
    "print(double(7))\n",
    "\n",
    "# Equivalent\n",
    "def double_again(n):\n",
    "    return n * 2\n",
    "print(double_again(7))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1d. Higher-order functions\n",
    "\n",
    "Functions that take functions as arguments. `sorted`, `map`, `filter`, `max`, `min` all accept a `key=` callable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cities = [\"Chiang Mai\", \"Pai\", \"Lampang\", \"Mae Hong Son\"]\n",
    "\n",
    "# Sort by length\n",
    "print(sorted(cities, key=len))\n",
    "\n",
    "# Sort by last letter\n",
    "print(sorted(cities, key=lambda c: c[-1]))\n",
    "\n",
    "# Longest city name\n",
    "print(max(cities, key=len))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1e. Type hints\n",
    "\n",
    "Optional annotations that document intent and help editors catch mistakes. Python doesn't enforce them at runtime — tools like `mypy` or `pyright` do."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def average(numbers: list[float]) -> float:\n",
    "    return sum(numbers) / len(numbers)\n",
    "\n",
    "print(average([10, 20, 30, 40]))\n",
    "\n",
    "# Combining: Optional value, default None\n",
    "def greet(name: str, greeting: str | None = None) -> str:\n",
    "    if greeting is None:\n",
    "        greeting = \"Sawadee\"\n",
    "    return f\"{greeting}, {name}!\"\n",
    "\n",
    "print(greet(\"Aroon\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1f. Decorators (brief)\n",
    "\n",
    "A decorator is a function that wraps another function to add behavior. The `@` syntax is shorthand for `func = decorator(func)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "def timed(func):\n",
    "    def wrapper(*args, **kwargs):\n",
    "        start = time.time()\n",
    "        result = func(*args, **kwargs)\n",
    "        print(f\"{func.__name__} took {time.time() - start:.3f}s\")\n",
    "        return result\n",
    "    return wrapper\n",
    "\n",
    "@timed\n",
    "def slow_add(a, b):\n",
    "    time.sleep(0.5)\n",
    "    return a + b\n",
    "\n",
    "print(slow_add(2, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Common built-in decorators you'll see: `@staticmethod`, `@classmethod`, `@property`, `@functools.cache`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Calling HTTP APIs\n",
    "\n",
    "An **API** (Application Programming Interface) is a way for your code to talk to a service over the network. HTTP APIs are the most common kind — you send a request to a URL, you get a response back, usually as JSON.\n",
    "\n",
    "The `requests` library is the standard. Install it once with:\n",
    "\n",
    "```\n",
    "pip install requests\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "# GET the weather for Chiang Mai (free, no auth, JSON format)\n",
    "url = \"https://wttr.in/Chiang+Mai?format=j1\"\n",
    "response = requests.get(url, timeout=10)\n",
    "\n",
    "print(\"Status code:\", response.status_code)\n",
    "print(\"Type:\", response.headers.get(\"content-type\"))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Parse the JSON body into a Python dict\n",
    "data = response.json()\n",
    "\n",
    "current = data[\"current_condition\"][0]\n",
    "print(f\"Temperature: {current['temp_C']}\\u00b0C\")\n",
    "print(f\"Conditions:  {current['weatherDesc'][0]['value']}\")\n",
    "print(f\"Humidity:    {current['humidity']}%\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Status codes\n",
    "\n",
    "Every HTTP response has a status code. Memorize the families, not every number:\n",
    "\n",
    "| Range | Meaning           |\n",
    "|-------|-------------------|\n",
    "| 2xx   | success           |\n",
    "| 3xx   | redirect          |\n",
    "| 4xx   | client error (your fault — bad URL, missing auth) |\n",
    "| 5xx   | server error (their fault) |\n",
    "\n",
    "`response.raise_for_status()` throws an exception on 4xx/5xx — combine it with `try`/`except` for clean error handling."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "def fetch_weather(city: str) -> dict | None:\n",
    "    url = f\"https://wttr.in/{city}?format=j1\"\n",
    "    try:\n",
    "        response = requests.get(url, timeout=10)\n",
    "        response.raise_for_status()\n",
    "        return response.json()\n",
    "    except requests.RequestException as e:\n",
    "        print(f\"Network error: {e}\")\n",
    "        return None\n",
    "\n",
    "data = fetch_weather(\"Chiang+Mai\")\n",
    "if data:\n",
    "    temp = data[\"current_condition\"][0][\"temp_C\"]\n",
    "    print(f\"Chiang Mai is {temp}\\u00b0C\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Other request methods\n",
    "\n",
    "- `requests.get(url, params={...})` — GET with query string\n",
    "- `requests.post(url, json={...})` — POST a JSON body\n",
    "- `requests.put(url, json={...})` — replace a resource\n",
    "- `requests.delete(url)` — delete\n",
    "\n",
    "Most APIs also require auth — usually a header like `Authorization: Bearer YOUR_KEY`:\n",
    "\n",
    "```python\n",
    "headers = {\"Authorization\": f\"Bearer {api_key}\"}\n",
    "requests.get(url, headers=headers)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Async — concurrency for I/O\n",
    "\n",
    "When your program waits for a network response, the CPU sits idle. **Async** lets you start multiple slow operations and wait for all of them together, instead of one at a time.\n",
    "\n",
    "**Async is for I/O-bound work** (network, disk, database). For CPU-heavy work like image processing, you want threads or multiple processes instead."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3a. `async def` and `await`\n",
    "\n",
    "An `async def` function returns a *coroutine* when called — it doesn't run yet. You run it with `await`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "\n",
    "async def fetch_city(name):\n",
    "    print(f\"  start {name}\")\n",
    "    await asyncio.sleep(1)   # pretend network call\n",
    "    print(f\"  done  {name}\")\n",
    "    return f\"{name} weather data\"\n",
    "\n",
    "# In Jupyter, `await` works at the top level (no asyncio.run needed)\n",
    "result = await fetch_city(\"Chiang Mai\")\n",
    "print(\"Got:\", result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Jupyter vs script:** Jupyter already runs an event loop, so you can use bare `await`. In a regular `.py` file you need to call `asyncio.run(main())` once at the top:\n",
    "\n",
    "```python\n",
    "async def main():\n",
    "    result = await fetch_city(\"Chiang Mai\")\n",
    "    print(result)\n",
    "\n",
    "asyncio.run(main())\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3b. The payoff — `asyncio.gather`\n",
    "\n",
    "Compare the two cells below. Each \"call\" sleeps for 1 second. Three of them, run **sequentially**, takes ~3 seconds. Run **concurrently** with `gather`, ~1 second."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "# Sequential: one after another\n",
    "start = time.time()\n",
    "a = await fetch_city(\"Chiang Mai\")\n",
    "b = await fetch_city(\"Lampang\")\n",
    "c = await fetch_city(\"Pai\")\n",
    "print(f\"Sequential: {time.time() - start:.2f}s\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Concurrent: all three start together, finish together\n",
    "start = time.time()\n",
    "a, b, c = await asyncio.gather(\n",
    "    fetch_city(\"Chiang Mai\"),\n",
    "    fetch_city(\"Lampang\"),\n",
    "    fetch_city(\"Pai\"),\n",
    ")\n",
    "print(f\"Concurrent: {time.time() - start:.2f}s\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice the print order: in the concurrent run, you see all three **start** messages first, then all three **done** messages. They're interleaved.\n",
    "\n",
    "### 3c. For real HTTP, use `aiohttp` or `httpx`\n",
    "\n",
    "`requests` is **synchronous** — calling it inside an async function blocks the whole event loop and defeats the point. For async HTTP, use `aiohttp` or `httpx` (`pip install aiohttp` or `pip install httpx`):\n",
    "\n",
    "```python\n",
    "import aiohttp, asyncio\n",
    "\n",
    "async def fetch(session, url):\n",
    "    async with session.get(url) as resp:\n",
    "        return await resp.json()\n",
    "\n",
    "async def main():\n",
    "    async with aiohttp.ClientSession() as session:\n",
    "        cities = [\"Chiang+Mai\", \"Lampang\", \"Pai\"]\n",
    "        urls = [f\"https://wttr.in/{c}?format=j1\" for c in cities]\n",
    "        results = await asyncio.gather(*(fetch(session, u) for u in urls))\n",
    "        for city, data in zip(cities, results):\n",
    "            temp = data[\"current_condition\"][0][\"temp_C\"]\n",
    "            print(f\"{city}: {temp}\\u00b0C\")\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Mini exercises\n",
    "\n",
    "1. Write a function `apply(func, value)` that calls `func(value)` and returns the result. Use it with a lambda to square the number 7.\n",
    "2. Using `requests`, fetch `https://wttr.in/Lampang?format=j1` and print just the temperature in Celsius. Use `try`/`except` so the cell doesn't crash if you're offline.\n",
    "3. Write an async function `tick(n)` that prints `n` once a second for 3 seconds (use `asyncio.sleep(1)`). Then use `asyncio.gather` to run `tick(\"A\")`, `tick(\"B\")`, `tick(\"C\")` concurrently and observe the interleaving."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 1: your code here\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 2: your code here\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Exercise 3: your code here\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "### Solutions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 1\n",
    "def apply(func, value):\n",
    "    return func(value)\n",
    "\n",
    "print(apply(lambda n: n * n, 7))   # 49"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 2\n",
    "import requests\n",
    "\n",
    "try:\n",
    "    r = requests.get(\"https://wttr.in/Lampang?format=j1\", timeout=10)\n",
    "    r.raise_for_status()\n",
    "    temp = r.json()[\"current_condition\"][0][\"temp_C\"]\n",
    "    print(f\"Lampang: {temp}\\u00b0C\")\n",
    "except requests.RequestException as e:\n",
    "    print(f\"Could not fetch: {e}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 3\n",
    "import asyncio\n",
    "\n",
    "async def tick(n):\n",
    "    for i in range(3):\n",
    "        print(f\"{n} tick {i+1}\")\n",
    "        await asyncio.sleep(1)\n",
    "\n",
    "await asyncio.gather(tick(\"A\"), tick(\"B\"), tick(\"C\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Where to go after this\n",
    "\n",
    "- **`httpx`** — modern HTTP client; sync *and* async in one API. A solid replacement for `requests` + `aiohttp`.\n",
    "- **`asyncio.Semaphore`** — limit concurrency (e.g., \"at most 5 requests at a time\").\n",
    "- **`asyncio.create_task`** — fire-and-forget a coroutine; useful for background work.\n",
    "- **`fastapi`** — build your own HTTP API with async support and automatic docs.\n",
    "- **`pydantic`** — typed data models, often paired with FastAPI.\n",
    "- **`functools`** — `cache`, `lru_cache`, `partial`, `reduce` — the standard library's higher-order toolkit."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
