diff --git a/pages/blog/one-day-build---play-outside.md b/pages/blog/one-day-build---play-outside.md
new file mode 100644
index 0000000000..1faa61562e
--- /dev/null
+++ b/pages/blog/one-day-build---play-outside.md
@@ -0,0 +1,474 @@
+---
+date: 2024-01-16 12:32:20
+templateKey: blog-post
+title: One Day Build - Play Outside
+slug: odb-play-outside
+tags:
+ - python
+published: False
+
+---
+
+Inspired by Adam Savage and his One Day builds on youtube. I often build
+things, and want to make them generally useful for others and over configure
+out of the gate. This project is purely for me inspired by a need I have.
+
+## !How-To
+
+This post will not directly show how to make a weather app, but document the
+process that I went through to make mine. It will show the tools that I used
+to make it, and the final result.
+
+## The Situation
+
+It often goes in our house ask dad while he is busy and he will probably just
+say yes without thinking much. This happens a lot when kids ask to go
+outside. I think sure, go for it, you will figure it out. Then my wife walks
+in and asks where they are, followed by, did you even check the weather, its
+-11 degrees outside right now.
+
+> I need a tool for this decision making process
+
+## Lungs
+
+You we have a family of not the most heathly lungs, we have my wife with lung
+cancer, one lung missing, and kids with asthma. We need to account for
+temperature, humidity, wind chill, and air quality before heading outside and
+seeing the repercussions of it later.
+
+## Final result
+
+So this is what I built, its a web app that checks the weather and air quality
+in your area and determines if its safe to go outside. It will even recommend
+limiting your time, or wearing a coat.
+
+[![](https://shots.wayl.one/shot/?url=https://play-outside.wayl.one&height=1200&width=600&scaled_width=600&scaled_height=1200&selectors=)](https://play-outside.wayl.one/)
+
+## The Stack
+
+This is a one day build, I have both kids at home from school, so this is
+realistically only like 2-3 hours at most, so this has to be chosen based on
+familiarity.
+
+- Docker
+- k8s
+- Python
+- FastAPI
+- tailwind
+- httpx
+- OpenWeatherMap API
+- ipwho.is
+
+This is the same stack (minue the apis) that I am using to build my startup
+fokais.com with. I am quite familiar with it and should be able to quickly
+make progress with it.
+
+``` shell
+❯ tree
+Permissions Size User Date Modified Git Name
+drwxr-xr-x - waylon 16 Jan 12:21 -N .
+.rw-r--r-- 3.8k waylon 16 Jan 12:24 -N ├── deployment.yaml
+.rw-r--r-- 278 waylon 16 Jan 12:21 -N ├── docker-compose.yml
+.rw-r--r-- 552 waylon 16 Jan 12:20 -N ├── Dockerfile
+.rw-r--r-- 15k waylon 15 Jan 14:37 -N ├── favicon.ico
+.rw-r--r-- 1.9k waylon 15 Jan 15:58 -N ├── justfile
+.rw-r--r-- 1.1k waylon 15 Jan 11:39 -N ├── LICENSE.txt
+.rw-r--r-- 51k waylon 15 Jan 14:38 -N ├── package-lock.json
+.rw-r--r-- 69 waylon 15 Jan 14:38 -N ├── package.json
+drwxr-xr-x - waylon 16 Jan 12:20 -N ├── play_outside
+.rw-r--r-- 138 waylon 16 Jan 12:21 -N │ ├── __about__.py
+.rw-r--r-- 115 waylon 15 Jan 11:39 -N │ ├── __init__.py
+.rw-r--r-- 7.5k waylon 16 Jan 08:14 -N │ ├── api.py
+drwxr-xr-x - waylon 15 Jan 21:30 -N │ ├── cli
+.rw-r--r-- 3.5k waylon 15 Jan 21:30 -N │ │ └── api.py
+.rw-r--r-- 2.8k waylon 16 Jan 12:20 -N │ ├── config.py
+.rw-r--r-- 3.0k waylon 15 Jan 14:35 -N │ ├── decorators.py
+.rw-r--r-- 51 waylon 15 Jan 14:50 -N │ └── queries.py
+.rw-r--r-- 2.2k waylon 15 Jan 15:16 -N ├── pyproject.toml
+.rw-r--r-- 506 waylon 15 Jan 11:39 -N ├── README.md
+drwxr-xr-x - waylon 15 Jan 14:39 -N ├── static
+.rw-r--r-- 21k waylon 16 Jan 08:10 -N │ ├── app.css
+.rw-r--r-- 15k waylon 15 Jan 14:37 -N │ ├── favicon.ico
+.rw-r--r-- 47k waylon 15 Jan 14:32 -N │ └── htmx.org@1.9.8
+drwxr-xr-x - waylon 15 Jan 21:04 -N ├── tailwind
+.rw-r--r-- 6.2k waylon 15 Jan 21:04 -N │ └── app.css
+.rw-r--r-- 360 waylon 15 Jan 21:04 -N ├── tailwind.config.js
+drwxr-xr-x - waylon 16 Jan 12:16 -N ├── templates
+.rw-r--r-- 2.5k waylon 16 Jan 12:16 -N │ ├── base.html
+.rw-r--r-- 2.2k waylon 16 Jan 12:11 -N │ ├── card.html
+.rw-r--r-- 151 waylon 15 Jan 21:07 -N │ ├── includestyles.html
+.rw-r--r-- 418 waylon 16 Jan 12:12 -N │ └── index.html
+drwxr-xr-x - waylon 15 Jan 11:39 -N └── tests
+.rw-r--r-- 115 waylon 15 Jan 11:39 -N └── __init__.py
+```
+
+## HTMX BTW
+
+I have been pairing up htmx with this stack quite a lot lately, and its
+fantastic, but honestly this idea just does not have a lot of endpoints, and I
+don't think it needs it for a one day build, just toss everything into one page
+and call it good.
+
+## Getting Weather Data
+
+The first thing we need is the feels_like or Apartment Temperature. A quick
+google search lead me to they have a very nice
+calculation for the feels like temerature already built in.
+
+
+
+
+
+Now using openweathermap, we can get the feels like temperature, by latitude
+and longitude.
+
+```python
+async def get_weather(lat_long):
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"https://api.openweathermap.org/data/2.5/weather?units=imperial&lat={lat_long['latitude']}&lon={lat_long['longitude']}&appid={config.open_weather_api_key}"
+ )
+ return response.json()
+```
+
+## Where are you??
+
+Since no one is going to know their current latitude and longitude we need a
+way to look this up for it to actually be useful. For this I leaned on
+
+
+
+
+
+
+``` python
+async def get_lat_long(ip_address):
+ if ip_address is None:
+ ip_address = "140.177.140.75"
+ async with httpx.AsyncClient() as client:
+ response = await client.get(f"https://ipwho.is/{ip_address}")
+ return response.json()
+```
+
+## Decision Tree
+
+For this I punted over to chatGPT to see what it came up with.
+
+``` prompt
+I am working on a web app to tell kids if its ok to play outside. It is
+targeted towards kids living in the midwest united states. I have the feels
+like temperature already. Set some values for the temperature
+
+too cold,
+no longer than 15 minutes,
+coats required,
+no longer than 15 minutes,
+too hot
+```
+
+What it came up with wasn't a terrible starting point, but its values
+essentially said kids cant play in the snow.
+
+``` python
+def determine_play_condition(feels_like_temperature):
+ if feels_like_temperature < 20:
+ return "It's too cold for extended play. Stay indoors and keep warm!"
+ elif 20 <= feels_like_temperature <= 32:
+ return "Perfect weather for snow play! Enjoy the winter wonderland!"
+ elif 32 < feels_like_temperature <= 40:
+ return "Coats and winter gear required for outdoor play. Stay cozy!"
+ elif 40 < feels_like_temperature <= 50:
+ return "You can play in the snow, but limit your time. It's getting warmer!"
+ else:
+ return "It's too warm for snow play. Find other fun activities indoors!"
+
+# Example usage:
+feels_like_temp = 25 # Replace with the actual feels like temperature
+message = determine_play_condition(feels_like_temp)
+print(message)
+```
+
+## Some Massaging Later
+
+My wife sent me over this image from
+[Tinkergarten](https://tinkergarten.com/pages/weather-watch), which is a chart
+made by the Iowa Department of Public Health. I used it as a guide to set some
+values, and added some conditions based on visibility and air quality index
+(aqi), which we have become all too familiar with over the past year with all
+the forest fires out Western US causing our lungs issues here in the Midwest.
+
+
+
+> (aqi) which we have become all too familiar with over the past year with all
+> the forest fires out Western US
+
+``` python
+def determine_play_condition(weather, aqi=0):
+ play_condition = PlayCondition()
+
+ feels_like_temperature = weather["main"]["feels_like"]
+ visibility = weather["visibility"]
+
+ play_condition.message += hours_till_sunset(weather)
+
+ if "after" in play_condition.message:
+ play_condition.color = "bg-red-500"
+
+ if visibility < 1000:
+ play_condition.message += "It's too foggy. Find better activities inside!"
+ play_condition.color = "bg-red-500"
+
+ if aqi > 150:
+ play_condition.message += "It's too polluted. Find better activities inside!"
+ play_condition.color = "bg-red-500"
+ elif aqi > 100:
+ play_condition.message += "limit your time outside due to the poor air quality"
+ play_condition.color = "bg-yellow-500"
+ elif aqi > 50:
+ play_condition.message += "Check the air quality outside at your discression."
+ play_condition.color = "bg-yellow-500"
+ else:
+ play_condition.message += ""
+
+ if feels_like_temperature < 10:
+ play_condition.message += "It's too cold. Stay indoors and keep warm!"
+ play_condition.color = "bg-red-500"
+ elif feels_like_temperature < 30:
+ play_condition.message += "You can play outside, but limit your time!"
+ play_condition.color = "bg-yellow-500"
+ elif feels_like_temperature < 40:
+ play_condition.message += (
+ "Coats and winter gear required for outdoor play. Stay cozy!"
+ )
+ elif feels_like_temperature < 50:
+ play_condition.message += "Grab a warm jacket and enjoy your time outside!"
+ elif feels_like_temperature < 60:
+ play_condition.message += "Grab some long sleeves and enjoy your time outside!"
+ elif feels_like_temperature > 90:
+ play_condition.message += (
+ "You can play outside, but limit your time in this heat!"
+ )
+ play_condition.color = "bg-yellow-500"
+ elif feels_like_temperature > 109:
+ play_condition.message += (
+ "It's too hot for outdoor play. Find cooler activities indoors!"
+ )
+ play_condition.color = "bg-red-500"
+ else:
+ play_condition.message += "Enjoy your time outside!"
+ return play_condition
+```
+
+## Pulling the data together
+
+Since I will be needing all of the data together upon every request I put
+together one `get_data` function to return a dict of all of the data.
+
+!!! note forecast
+ I pulled the forecast endpoint from openweathermap as well, it looks like a
+ stripped down version of the regular weather endpoint, but every few hours
+ over the course of the next 5 days.
+
+``` python
+async def get_data(request: Request):
+ user_ip = request.headers.get("CF-Connecting-IP")
+ lat_long = await get_lat_long(user_ip)
+ weather = await get_weather(lat_long)
+ forecast = await get_forecast(lat_long)
+ air_quality = await get_air_quality(lat_long)
+ weather["play_condition"] = determine_play_condition(
+ weather,
+ air_quality["list"][0]["main"]["aqi"],
+ )
+
+ forecast = [
+ {"play_condition": determine_play_condition(x), **x}
+ for x in forecast
+ if datetime.fromtimestamp(x["dt"]).hour >= 6
+ and datetime.fromtimestamp(x["dt"]).hour <= 21
+ ]
+
+ return {
+ "request.client": request.client,
+ "request.client.host": request.client.host,
+ "user_ip": user_ip,
+ "lat_long": lat_long,
+ "weather": weather,
+ "forecast": forecast,
+ "air_quality": air_quality,
+ "sunset": weather["sys"]["sunset"],
+ }
+```
+
+## FastAPI
+
+Fastapi here is a great framework, it uses pydantic to validate the data
+returned from the api, has a great dependency management system.
+
+I am going to use none of that, all I need is one TemplateResponse using jinja.
+For good measure, I'll toss in a `/metadata` route that returns the data.
+
+``` python
+from fastapi import FastAPI
+app = FastAPI()
+
+
+@app.get("/")
+@no_cache
+async def get_home(request: Request):
+ data = await get_data(request)
+ return config.templates.TemplateResponse("index.html", {"request": request, **data})
+
+@app.get("/metadata")
+async def root(
+ request: Request,
+):
+ return await get_data(request)
+```
+
+## No Cache Header
+
+I had some issues with cloudflare caching me and not letting me hit the api
+everytime. I've ran into this several times in the past, so I went to the
+cloudflare dashboard, manually busted the cache for the home route and popped a
+`no_cache` decorator on the `get_home` route.
+
+``` python
+def no_cache(func):
+ not_cached_routes.append(f"{func.__module__}.{func.__name__}")
+
+ @wraps(func)
+ async def wrapper(*args, request: Request, **kwargs):
+ # my_header will be now available in decorator
+ if "request" in signature(func).parameters:
+ kwargs["request"] = request
+
+ if inspect.iscoroutinefunction(func):
+ response = await func(*args, **kwargs)
+ else:
+ response = func(*args, **kwargs)
+
+ response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
+ response.headers["Pragma"] = "no-cache"
+ response.headers["Expires"] = "0"
+ return response
+
+ return wrapper
+
+@app.get("/")
+@no_cache
+async def get_home(request: Request):
+ data = await get_data(request)
+ return config.templates.TemplateResponse("index.html", {"request": request, **data})
+```
+
+This solved my caching issues.
+
+## Templates
+
+I used jinja for templating its built right into FastAPI.
+
+``` python
+from fastapi.templating import Jinja2Templates
+
+templates = Jinja2Templates(directory="templates")
+```
+
+This will get you by for quite awhile, and I probably could deal with templates
+working just like this for a one day build, but I have some nice feautures that
+I like from other projects, and at least one specific to just this project.
+Once they are in my config object, I use them like so.
+
+!!! Note
+ The `request` parameter is a requirement for all templates.
+
+``` python
+@app.get("/")
+@no_cache
+async def get_home(request: Request):
+ data = await get_data(request)
+ return config.templates.TemplateResponse("index.html", {"request": request, **data})
+```
+
+### Filters
+
+I need a nice way to convert the openweathermap timestamps to human readable values.
+
+``` python
+from datetime import datetime
+from datetime import timezone
+
+templates.env.filters["timestamp"] = lambda u: datetime.fromtimestamp(
+ u, tz=timezone.utc
+).strftime("%B %d, %Y")
+```
+
+``` python
+import os
+from rich.console import Console
+
+console = Console()
+
+templates.env.globals["https_url_for"] = https_url_for
+if os.environ.get("ENV") in ["dev", "qa", "prod"]:
+ templates.env.globals["url_for"] = https_url_for
+ console.print("Using HTTPS")
+else:
+ console.print("Using HTTP")
+
+return templates
+```
+
+``` python
+import os
+from datetime import datetime
+from datetime import timezone
+
+from fastapi.templating import Jinja2Templates
+from rich.console import Console
+
+console = Console()
+
+def get_templates(config: BaseSettings) -> Jinja2Templates:
+ templates = Jinja2Templates(directory="templates")
+ templates.env.filters["quote_plus"] = lambda u: quote_plus(str(u))
+ templates.env.filters["timestamp"] = lambda u: datetime.fromtimestamp(
+ u, tz=timezone.utc
+ ).strftime("%B %d, %Y")
+ templates.env.globals["https_url_for"] = https_url_for
+ templates.env.globals["config"] = config
+ templates.env.globals["datetime"] = datetime
+ console.print(f'Using environment: {os.environ.get("ENV")}')
+
+ if os.environ.get("ENV") in ["dev", "qa", "prod"]:
+ templates.env.globals["url_for"] = https_url_for
+ console.print("Using HTTPS")
+ else:
+ console.print("Using HTTP")
+
+ return templates
+```
+
+## Styles
+
+## Deployment
+
+## Docker
+
+## CI
+
+## K8s