├── ch08-deployment ├── .idea │ ├── .name │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── demo ch08-deployment.iml ├── requirements.txt ├── static │ ├── img │ │ ├── cloud.png │ │ └── favicon.ico │ └── css │ │ ├── docs.css │ │ └── theme.css ├── settings_template.json ├── models │ ├── location.py │ ├── validation_error.py │ └── reports.py ├── views │ └── home.py ├── server │ ├── nginx │ │ └── weather.nginx │ ├── units │ │ └── weather.service │ └── scripts │ │ └── server_setup.sh ├── services │ ├── report_service.py │ └── openweather_service.py ├── bin │ └── reportapp.py ├── api │ └── weather_api.py ├── infrastructure │ └── weather_cache.py ├── main.py └── templates │ ├── shared │ └── layout.html │ └── home │ └── index.html ├── ch03-first-api ├── requirements.txt ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── ch03-first-api.iml └── main.py ├── ch04-language-foundations ├── models │ ├── requirements.txt │ ├── orders_v2.py │ └── orders_v1.py ├── async │ ├── async_scrape │ │ ├── requirements.txt │ │ └── program.py │ └── sync_scrape │ │ ├── requirements.txt │ │ └── program.py ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── ch04-language-foundations.iml ├── asgi │ └── asgi_summary.py └── types │ ├── no_types_program.py │ └── types_program.py ├── requirements.txt ├── ch05-a-realistic-api ├── requirements.txt ├── static │ ├── img │ │ ├── cloud.png │ │ └── favicon.ico │ └── css │ │ ├── docs.css │ │ └── theme.css ├── settings_template.json ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── ch05-a-realistic-api.iml ├── models │ └── location.py ├── api │ └── weather_api.py ├── views │ └── home.py ├── services │ └── openweather_service.py ├── main.py └── templates │ ├── shared │ └── layout.html │ └── home │ └── index.html ├── ch06-error-handling-and-perf ├── requirements.txt ├── static │ ├── img │ │ ├── cloud.png │ │ └── favicon.ico │ └── css │ │ ├── docs.css │ │ └── theme.css ├── settings_template.json ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── ch06-error-handling-and-perf.iml ├── models │ ├── location.py │ └── validation_error.py ├── views │ └── home.py ├── api │ └── weather_api.py ├── main.py ├── infrastructure │ └── weather_cache.py ├── templates │ ├── shared │ │ └── layout.html │ └── home │ │ └── index.html └── services │ └── openweather_service.py ├── ch07-inbound-data ├── requirements.txt ├── static │ ├── img │ │ ├── cloud.png │ │ └── favicon.ico │ └── css │ │ ├── docs.css │ │ └── theme.css ├── settings_template.json ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── ch07-inbound-data.iml ├── models │ ├── location.py │ ├── validation_error.py │ └── reports.py ├── views │ └── home.py ├── services │ ├── report_service.py │ └── openweather_service.py ├── bin │ └── reportapp.py ├── api │ └── weather_api.py ├── infrastructure │ └── weather_cache.py ├── main.py └── templates │ ├── shared │ └── layout.html │ └── home │ └── index.html ├── README.md └── .gitignore /ch08-deployment/.idea/.name: -------------------------------------------------------------------------------- 1 | demo ch08-deployment -------------------------------------------------------------------------------- /ch03-first-api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | 4 | -------------------------------------------------------------------------------- /ch04-language-foundations/models/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | pydantic 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | requests 7 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/async_scrape/requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | colorama 3 | httpx 4 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/sync_scrape/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | bs4 3 | colorama 4 | 5 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | -------------------------------------------------------------------------------- /ch08-deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | requests 7 | -------------------------------------------------------------------------------- /ch07-inbound-data/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | requests 7 | -------------------------------------------------------------------------------- /ch08-deployment/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch08-deployment/static/img/cloud.png -------------------------------------------------------------------------------- /ch07-inbound-data/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch07-inbound-data/static/img/cloud.png -------------------------------------------------------------------------------- /ch07-inbound-data/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch07-inbound-data/static/img/favicon.ico -------------------------------------------------------------------------------- /ch08-deployment/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch08-deployment/static/img/favicon.ico -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch05-a-realistic-api/static/img/cloud.png -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch05-a-realistic-api/static/img/favicon.ico -------------------------------------------------------------------------------- /ch07-inbound-data/settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org", 3 | "action": "copy this to settings.json with real values." 4 | } -------------------------------------------------------------------------------- /ch08-deployment/settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org", 3 | "action": "copy this to settings.json with real values." 4 | } -------------------------------------------------------------------------------- /ch05-a-realistic-api/settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org", 3 | "action": "copy this to settings.json with real values." 4 | } -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch06-error-handling-and-perf/static/img/cloud.png -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/settings_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org", 3 | "action": "copy this to settings.json with real values." 4 | } -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch06-error-handling-and-perf/static/img/favicon.ico -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern APIs with FastAPI course 2 | 3 | Coming November 2020 from Talk Python Training. Sign up to get notified at [talkpython.fm/friends](https://talkpython.fm/friends). 4 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch07-inbound-data/models/location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Location(BaseModel): 7 | city: str 8 | state: Optional[str] = None 9 | country: str = 'US' 10 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch08-deployment/models/location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Location(BaseModel): 7 | city: str 8 | state: Optional[str] = None 9 | country: str = 'US' 10 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/models/location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Location(BaseModel): 7 | city: str 8 | state: Optional[str] = None 9 | country: str = 'US' 10 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/models/location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel 4 | 5 | 6 | class Location(BaseModel): 7 | city: str 8 | state: Optional[str] = None 9 | country: str = 'US' 10 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ch07-inbound-data/models/validation_error.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | def __init__(self, error_msg: str, status_code: int): 3 | super().__init__(error_msg) 4 | 5 | self.status_code = status_code 6 | self.error_msg = error_msg 7 | -------------------------------------------------------------------------------- /ch08-deployment/models/validation_error.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | def __init__(self, error_msg: str, status_code: int): 3 | super().__init__(error_msg) 4 | 5 | self.status_code = status_code 6 | self.error_msg = error_msg 7 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/models/validation_error.py: -------------------------------------------------------------------------------- 1 | class ValidationError(Exception): 2 | def __init__(self, error_msg: str, status_code: int): 3 | super().__init__(error_msg) 4 | 5 | self.status_code = status_code 6 | self.error_msg = error_msg 7 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ch07-inbound-data/models/reports.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | from models.location import Location 8 | 9 | 10 | class ReportSubmittal(BaseModel): 11 | description: str 12 | location: Location 13 | 14 | 15 | class Report(ReportSubmittal): 16 | id: str 17 | created_date: Optional[datetime.datetime] 18 | -------------------------------------------------------------------------------- /ch08-deployment/models/reports.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | from models.location import Location 8 | 9 | 10 | class ReportSubmittal(BaseModel): 11 | description: str 12 | location: Location 13 | 14 | 15 | class Report(ReportSubmittal): 16 | id: str 17 | created_date: Optional[datetime.datetime] 18 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/api/weather_api.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import fastapi 4 | from fastapi import Depends 5 | 6 | from models.location import Location 7 | from services import openweather_service 8 | 9 | router = fastapi.APIRouter() 10 | 11 | 12 | @router.get('/api/weather/{city}') 13 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'): 14 | report = await openweather_service.get_report_async(loc.city, loc.state, loc.country, units) 15 | 16 | return report 17 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/views/home.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | from starlette.requests import Request 3 | from starlette.templating import Jinja2Templates 4 | 5 | templates = Jinja2Templates('templates') 6 | router = fastapi.APIRouter() 7 | 8 | 9 | @router.get('/') 10 | def index(request: Request): 11 | return templates.TemplateResponse('home/index.html', {'request': request}) 12 | 13 | 14 | @router.get('/favicon.ico') 15 | def favicon(): 16 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico') 17 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/views/home.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | from starlette.requests import Request 3 | from starlette.templating import Jinja2Templates 4 | 5 | templates = Jinja2Templates('templates') 6 | router = fastapi.APIRouter() 7 | 8 | 9 | @router.get('/') 10 | def index(request: Request): 11 | return templates.TemplateResponse('home/index.html', {'request': request}) 12 | 13 | 14 | @router.get('/favicon.ico') 15 | def favicon(): 16 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico') 17 | -------------------------------------------------------------------------------- /ch04-language-foundations/asgi/asgi_summary.py: -------------------------------------------------------------------------------- 1 | # Nothing to actually run, just explore these things. 2 | 3 | # What is ASGI 4 | 5 | # WSGI 6 | def request(environ, start_response): 7 | r = start_response(environ) 8 | # ... 9 | return r 10 | 11 | 12 | # ASGI 13 | async def app(scope, receive, send): 14 | r = await receive(scope) 15 | # ... 16 | return await send(r, scope) 17 | 18 | 19 | # Resources 20 | # https://github.com/florimondmanca/awesome-asgi 21 | 22 | 23 | # Server 24 | # uvicorn - https://www.uvicorn.org/ 25 | -------------------------------------------------------------------------------- /ch04-language-foundations/.idea/ch04-language-foundations.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/services/openweather_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import httpx 3 | 4 | api_key: Optional[str] = None 5 | 6 | 7 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict: 8 | if state: 9 | q = f'{city},{state},{country}' 10 | else: 11 | q = f'{city},{country}' 12 | 13 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}' 14 | 15 | async with httpx.AsyncClient() as client: 16 | resp = await client.get(url) 17 | resp.raise_for_status() 18 | 19 | data = resp.json() 20 | forecast = data['main'] 21 | return forecast 22 | -------------------------------------------------------------------------------- /ch04-language-foundations/models/orders_v2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List, Optional 3 | 4 | from dateutil.parser import parse 5 | from pydantic import BaseModel 6 | 7 | order_json = { 8 | 'item_id': '123', 9 | 'created_date': '2002-11-24 12:22', 10 | 'pages_visited': [1, 2, '3'], 11 | 'price': 17.22 12 | } 13 | 14 | 15 | class Order(BaseModel): 16 | item_id: int 17 | created_date: Optional[datetime.datetime] 18 | pages_visited: List[int] = [] 19 | price: float 20 | 21 | 22 | o = Order(**order_json) 23 | print(o) 24 | 25 | 26 | # Default for JSON post 27 | # Can be done for others with mods. 28 | def order_api(order: Order): 29 | pass 30 | -------------------------------------------------------------------------------- /ch07-inbound-data/views/home.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | from starlette.requests import Request 3 | from starlette.templating import Jinja2Templates 4 | 5 | from services import report_service 6 | 7 | templates = Jinja2Templates('templates') 8 | router = fastapi.APIRouter() 9 | 10 | 11 | @router.get('/', include_in_schema=False) 12 | async def index(request: Request): 13 | events = await report_service.get_reports() 14 | data = {'request': request, 'events': events} 15 | 16 | return templates.TemplateResponse('home/index.html', data) 17 | 18 | 19 | @router.get('/favicon.ico', include_in_schema=False) 20 | def favicon(): 21 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico') 22 | -------------------------------------------------------------------------------- /ch08-deployment/views/home.py: -------------------------------------------------------------------------------- 1 | import fastapi 2 | from starlette.requests import Request 3 | from starlette.templating import Jinja2Templates 4 | 5 | from services import report_service 6 | 7 | templates = Jinja2Templates('templates') 8 | router = fastapi.APIRouter() 9 | 10 | 11 | @router.get('/', include_in_schema=False) 12 | async def index(request: Request): 13 | events = await report_service.get_reports() 14 | data = {'request': request, 'events': events} 15 | 16 | return templates.TemplateResponse('home/index.html', data) 17 | 18 | 19 | @router.get('/favicon.ico', include_in_schema=False) 20 | def favicon(): 21 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico') 22 | -------------------------------------------------------------------------------- /ch08-deployment/static/css/docs.css: -------------------------------------------------------------------------------- 1 | .request { 2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 3 | font-weight: bold; 4 | border: 1px solid gray; 5 | border-radius: 5px; 6 | padding: 10px; 7 | font-size: 24px; 8 | } 9 | 10 | .get { 11 | color: #2b542c; 12 | background-color: #beffbd; 13 | } 14 | 15 | .post { 16 | color: #ae5900; 17 | background-color: #ffc79d; 18 | } 19 | 20 | .response_formats span { 21 | font-weight: bold; 22 | color: darkred; 23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 24 | } 25 | 26 | pre { 27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 28 | } 29 | 30 | ul li { 31 | font-size: 18px; 32 | margin-bottom: 10px; 33 | } -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/css/docs.css: -------------------------------------------------------------------------------- 1 | .request { 2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 3 | font-weight: bold; 4 | border: 1px solid gray; 5 | border-radius: 5px; 6 | padding: 10px; 7 | font-size: 24px; 8 | } 9 | 10 | .get { 11 | color: #2b542c; 12 | background-color: #beffbd; 13 | } 14 | 15 | .post { 16 | color: #ae5900; 17 | background-color: #ffc79d; 18 | } 19 | 20 | .response_formats span { 21 | font-weight: bold; 22 | color: darkred; 23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 24 | } 25 | 26 | pre { 27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 28 | } 29 | 30 | ul li { 31 | font-size: 18px; 32 | margin-bottom: 10px; 33 | } -------------------------------------------------------------------------------- /ch07-inbound-data/static/css/docs.css: -------------------------------------------------------------------------------- 1 | .request { 2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 3 | font-weight: bold; 4 | border: 1px solid gray; 5 | border-radius: 5px; 6 | padding: 10px; 7 | font-size: 24px; 8 | } 9 | 10 | .get { 11 | color: #2b542c; 12 | background-color: #beffbd; 13 | } 14 | 15 | .post { 16 | color: #ae5900; 17 | background-color: #ffc79d; 18 | } 19 | 20 | .response_formats span { 21 | font-weight: bold; 22 | color: darkred; 23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 24 | } 25 | 26 | pre { 27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 28 | } 29 | 30 | ul li { 31 | font-size: 18px; 32 | margin-bottom: 10px; 33 | } -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/static/css/docs.css: -------------------------------------------------------------------------------- 1 | .request { 2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 3 | font-weight: bold; 4 | border: 1px solid gray; 5 | border-radius: 5px; 6 | padding: 10px; 7 | font-size: 24px; 8 | } 9 | 10 | .get { 11 | color: #2b542c; 12 | background-color: #beffbd; 13 | } 14 | 15 | .post { 16 | color: #ae5900; 17 | background-color: #ffc79d; 18 | } 19 | 20 | .response_formats span { 21 | font-weight: bold; 22 | color: darkred; 23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 24 | } 25 | 26 | pre { 27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 28 | } 29 | 30 | ul li { 31 | font-size: 18px; 32 | margin-bottom: 10px; 33 | } -------------------------------------------------------------------------------- /ch08-deployment/server/nginx/weather.nginx: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name weatherapi.talkpython.com 128.199.4.0; 4 | server_tokens off; 5 | charset utf-8; 6 | 7 | location /static { 8 | gzip on; 9 | gzip_buffers 8 256k; 10 | 11 | alias /apps/app_repo/ch08-deployment/static; 12 | expires 365d; 13 | } 14 | location / { 15 | try_files $uri @yourapplication; 16 | } 17 | location @yourapplication { 18 | gzip on; 19 | gzip_buffers 8 256k; 20 | 21 | proxy_pass http://127.0.0.1:8000; 22 | proxy_set_header Host $host; 23 | proxy_set_header X-Real-IP $remote_addr; 24 | proxy_set_header X-Forwarded-Protocol $scheme; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/api/weather_api.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import fastapi 4 | from fastapi import Depends 5 | 6 | from models.location import Location 7 | from models.validation_error import ValidationError 8 | from services import openweather_service 9 | 10 | router = fastapi.APIRouter() 11 | 12 | 13 | @router.get('/api/weather/{city}') 14 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'): 15 | 16 | try: 17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units) 18 | except ValidationError as ve: 19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code) 20 | except Exception as x: 21 | return fastapi.Response(content=str(x), status_code=500) 22 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/ch03-first-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /ch08-deployment/services/report_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from typing import List 4 | 5 | from models.location import Location 6 | from models.reports import Report 7 | 8 | __reports: List[Report] = [] 9 | 10 | 11 | async def get_reports() -> List[Report]: 12 | 13 | # Would be an async call here. 14 | return list(__reports) 15 | 16 | 17 | async def add_report(description: str, location: Location) -> Report: 18 | now = datetime.datetime.now() 19 | report = Report( 20 | id=str(uuid.uuid4()), 21 | location=location, 22 | description=description, 23 | created_date=now) 24 | 25 | # Simulate saving to the DB. 26 | # Would be an async call here. 27 | __reports.append(report) 28 | 29 | __reports.sort(key=lambda r: r.created_date, reverse=True) 30 | 31 | return report 32 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/ch05-a-realistic-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /ch07-inbound-data/services/report_service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from typing import List 4 | 5 | from models.location import Location 6 | from models.reports import Report 7 | 8 | __reports: List[Report] = [] 9 | 10 | 11 | async def get_reports() -> List[Report]: 12 | 13 | # Would be an async call here. 14 | return list(__reports) 15 | 16 | 17 | async def add_report(description: str, location: Location) -> Report: 18 | now = datetime.datetime.now() 19 | report = Report( 20 | id=str(uuid.uuid4()), 21 | location=location, 22 | description=description, 23 | created_date=now) 24 | 25 | # Simulate saving to the DB. 26 | # Would be an async call here. 27 | __reports.append(report) 28 | 29 | __reports.sort(key=lambda r: r.created_date, reverse=True) 30 | 31 | return report 32 | -------------------------------------------------------------------------------- /ch08-deployment/server/units/weather.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=gunicorn uvicorn service for Weather Service API 3 | After=syslog.target 4 | 5 | [Service] 6 | ExecStart=/apps/venv/bin/gunicorn -b 127.0.0.1:8000 -w 4 -k uvicorn.workers.UvicornWorker main:api --name weather_svc --chdir /apps/app_repo/ch08-deployment --access-logfile /apps/logs/weather_api/access.log --error-logfile /apps/logs/weather_api/errors.log --user apiuser 7 | 8 | # \/ \/ <- Added post recording for better restart perf. 9 | ExecReload=/bin/kill -s HUP $MAINPID 10 | KillMode=mixed 11 | TimeoutStopSec=5 12 | PrivateTmp=true 13 | # /\ /\ <- Added post recording for better restart perf. 14 | 15 | # Requires systemd version 211 or newer 16 | RuntimeDirectory=/apps/app_repo/ch08-deployment 17 | Restart=always 18 | Type=notify 19 | StandardError=syslog 20 | NotifyAccess=all 21 | 22 | 23 | [Install] 24 | WantedBy=multi-user.target 25 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/demo ch08-deployment.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/.idea/ch06-error-handling-and-perf.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 18 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /ch07-inbound-data/.idea/ch07-inbound-data.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /ch04-language-foundations/types/no_types_program.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Item = namedtuple("Item", "name, value") 4 | 5 | running_max = None 6 | 7 | 8 | def counter(items): 9 | global running_max 10 | total = 0 11 | 12 | for i in items: 13 | total += i.value 14 | 15 | if not running_max or total > running_max: 16 | running_max = total 17 | 18 | return total 19 | 20 | 21 | def main(): 22 | print("Let's create some items") 23 | 24 | dinner_items = [Item('Pizza', 20), Item('Beer', 9), Item('Beer', 9)] 25 | breakfast_items = [Item('Pancakes', 11), Item('Bacon', 4), Item('Coffee', 3), Item('Coffee', 3), Item('Scone', 2)] 26 | 27 | dinner_total = counter(dinner_items) 28 | print(f"Dinner was ${dinner_total:,.02f}") 29 | 30 | breakfast_total = counter(breakfast_items) 31 | print(f"Breakfast was ${breakfast_total:,.02f}") 32 | 33 | print(f"Today your most expensive meal costs ${running_max:.02f}") 34 | 35 | 36 | if __name__ == '__main__': 37 | main() 38 | -------------------------------------------------------------------------------- /ch03-first-api/main.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import fastapi 4 | import uvicorn 5 | 6 | api = fastapi.FastAPI() 7 | 8 | 9 | @api.get('/') 10 | def index(): 11 | body = "" \ 12 | "" \ 13 | "

Welcome to the API

" \ 14 | "
" \ 15 | "Try it: /api/calculate?x=7&y=11" \ 16 | "
" \ 17 | "" \ 18 | "" 19 | 20 | return fastapi.responses.HTMLResponse(content=body) 21 | 22 | 23 | @api.get('/api/calculate') 24 | def calculate(x: int, y: int, z: Optional[int] = None): 25 | if z == 0: 26 | return fastapi.responses.JSONResponse( 27 | content={"error": "ERROR: Z cannot be zero."}, 28 | status_code=400) 29 | 30 | value = x + y 31 | 32 | if z is not None: 33 | value /= z 34 | 35 | return { 36 | 'x': x, 37 | 'y': y, 38 | 'z': z, 39 | 'value': value 40 | } 41 | 42 | 43 | uvicorn.run(api, port=8000, host="127.0.0.1") 44 | -------------------------------------------------------------------------------- /ch04-language-foundations/types/types_program.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from typing import Optional, Iterable 3 | 4 | Item = namedtuple("Item", "name, value") 5 | 6 | running_max: Optional[int] = None 7 | 8 | 9 | def counter(items: Iterable[Item]) -> int: 10 | global running_max 11 | 12 | total = 0 13 | 14 | for i in items: 15 | total += i.value 16 | 17 | if not running_max or total > running_max: 18 | running_max = total 19 | 20 | return total 21 | 22 | 23 | def main(): 24 | print("Let's create some items") 25 | 26 | dinner_items = [Item('Pizza', 20), Item('Beer', 9), Item('Beer', 9)] 27 | breakfast_items = [Item('Pancakes', 11), Item('Bacon', 4), Item('Coffee', 3), Item('Coffee', 3), Item('Scone', 2)] 28 | 29 | dinner_total = counter(dinner_items) 30 | print(f"Dinner was ${dinner_total:,.02f}") 31 | 32 | breakfast_total = counter(breakfast_items) 33 | print(f"Breakfast was ${breakfast_total:,.02f}") 34 | 35 | print(f"Today your most expensive meal costs ${running_max:.02f}") 36 | 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import fastapi 5 | import uvicorn 6 | from starlette.staticfiles import StaticFiles 7 | 8 | from api import weather_api 9 | from services import openweather_service 10 | from views import home 11 | 12 | api = fastapi.FastAPI() 13 | 14 | 15 | def configure(): 16 | configure_routing() 17 | configure_api_keys() 18 | 19 | 20 | def configure_api_keys(): 21 | file = Path('settings.json').absolute() 22 | if not file.exists(): 23 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json") 24 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json") 25 | 26 | with open('settings.json') as fin: 27 | settings = json.load(fin) 28 | openweather_service.api_key = settings.get('api_key') 29 | 30 | 31 | def configure_routing(): 32 | api.mount('/static', StaticFiles(directory='static'), name='static') 33 | api.include_router(home.router) 34 | api.include_router(weather_api.router) 35 | 36 | 37 | if __name__ == '__main__': 38 | configure() 39 | uvicorn.run(api, port=8000, host='127.0.0.1') 40 | else: 41 | configure() 42 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import fastapi 5 | import uvicorn 6 | from starlette.staticfiles import StaticFiles 7 | 8 | from api import weather_api 9 | from services import openweather_service 10 | from views import home 11 | 12 | api = fastapi.FastAPI() 13 | 14 | 15 | def configure(): 16 | configure_routing() 17 | configure_api_keys() 18 | 19 | 20 | def configure_api_keys(): 21 | file = Path('settings.json').absolute() 22 | if not file.exists(): 23 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json") 24 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json") 25 | 26 | with open('settings.json') as fin: 27 | settings = json.load(fin) 28 | openweather_service.api_key = settings.get('api_key') 29 | 30 | 31 | def configure_routing(): 32 | api.mount('/static', StaticFiles(directory='static'), name='static') 33 | api.include_router(home.router) 34 | api.include_router(weather_api.router) 35 | 36 | 37 | if __name__ == '__main__': 38 | configure() 39 | uvicorn.run(api, port=8000, host='127.0.0.1') 40 | else: 41 | configure() 42 | -------------------------------------------------------------------------------- /ch07-inbound-data/bin/reportapp.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def main(): 5 | choice = input("[R]eport weather or [s]ee reports? ") 6 | while choice: 7 | if choice.lower().strip() == 'r': 8 | report_event() 9 | elif choice.lower().strip() == 's': 10 | see_events() 11 | else: 12 | print(f"Don't know what to do with {choice}.") 13 | 14 | choice = input("[R]eport weather or [s]ee reports? ") 15 | 16 | 17 | def report_event(): 18 | desc = input("What is happening now? ") 19 | city = input("What city? ") 20 | 21 | data = { 22 | "description": desc, 23 | "location": { 24 | "city": city 25 | } 26 | } 27 | 28 | url = "http://127.0.0.1:8000/api/reports" 29 | resp = requests.post(url, json=data) 30 | resp.raise_for_status() 31 | 32 | result = resp.json() 33 | print(f"Reported new event: {result.get('id')}") 34 | 35 | 36 | def see_events(): 37 | url = "http://127.0.0.1:8000/api/reports" 38 | resp = requests.get(url) 39 | resp.raise_for_status() 40 | 41 | data = resp.json() 42 | for r in data: 43 | print(f"{r.get('location').get('city')} has {r.get('description')}") 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /ch08-deployment/bin/reportapp.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def main(): 5 | choice = input("[R]eport weather or [s]ee reports? ") 6 | while choice: 7 | if choice.lower().strip() == 'r': 8 | report_event() 9 | elif choice.lower().strip() == 's': 10 | see_events() 11 | else: 12 | print(f"Don't know what to do with {choice}.") 13 | 14 | choice = input("[R]eport weather or [s]ee reports? ") 15 | 16 | 17 | def report_event(): 18 | desc = input("What is happening now? ") 19 | city = input("What city? ") 20 | 21 | data = { 22 | "description": desc, 23 | "location": { 24 | "city": city 25 | } 26 | } 27 | 28 | url = "http://127.0.0.1:8000/api/reports" 29 | resp = requests.post(url, json=data) 30 | resp.raise_for_status() 31 | 32 | result = resp.json() 33 | print(f"Reported new event: {result.get('id')}") 34 | 35 | 36 | def see_events(): 37 | url = "http://127.0.0.1:8000/api/reports" 38 | resp = requests.get(url) 39 | resp.raise_for_status() 40 | 41 | data = resp.json() 42 | for r in data: 43 | print(f"{r.get('location').get('city')} has {r.get('description')}") 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/sync_scrape/program.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import requests 4 | import bs4 5 | from colorama import Fore 6 | 7 | 8 | def get_html(episode_number: int) -> str: 9 | print(Fore.YELLOW + f"Getting HTML for episode {episode_number}", flush=True) 10 | 11 | url = f'https://talkpython.fm/{episode_number}' 12 | resp = requests.get(url) 13 | resp.raise_for_status() 14 | 15 | return resp.text 16 | 17 | 18 | def get_title(html: str, episode_number: int) -> str: 19 | print(Fore.CYAN + f"Getting TITLE for episode {episode_number}", flush=True) 20 | soup = bs4.BeautifulSoup(html, 'html.parser') 21 | header = soup.select_one('h1') 22 | if not header: 23 | return "MISSING" 24 | 25 | return header.text.strip() 26 | 27 | 28 | def main(): 29 | t0 = datetime.datetime.now() 30 | get_title_range() 31 | dt = datetime.datetime.now() - t0 32 | print(f"Done in {dt.total_seconds():.2f} sec.") 33 | 34 | 35 | def get_title_range(): 36 | # Please keep this range pretty small to not DDoS my site. ;) 37 | for n in range(270, 280): 38 | html = get_html(n) 39 | title = get_title(html, n) 40 | print(Fore.WHITE + f"Title found: {title}", flush=True) 41 | 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /ch08-deployment/api/weather_api.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | import fastapi 4 | from fastapi import Depends 5 | 6 | from models.location import Location 7 | from models.reports import Report, ReportSubmittal 8 | from models.validation_error import ValidationError 9 | from services import openweather_service, report_service 10 | 11 | router = fastapi.APIRouter() 12 | 13 | 14 | @router.get('/api/weather/{city}') 15 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'): 16 | try: 17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units) 18 | except ValidationError as ve: 19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code) 20 | except Exception as x: 21 | return fastapi.Response(content=str(x), status_code=500) 22 | 23 | 24 | @router.get('/api/reports', name='all_reports', response_model=List[Report]) 25 | async def reports_get() -> List[Report]: 26 | # await report_service.add_report("A", Location(city="Portland")) 27 | # await report_service.add_report("B", Location(city="NYC")) 28 | return await report_service.get_reports() 29 | 30 | 31 | @router.post('/api/reports', name='add_report', status_code=201, response_model=Report) 32 | async def reports_post(report_submittal: ReportSubmittal) -> Report: 33 | d = report_submittal.description 34 | loc = report_submittal.location 35 | 36 | return await report_service.add_report(d, loc) 37 | -------------------------------------------------------------------------------- /ch07-inbound-data/api/weather_api.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List 2 | 3 | import fastapi 4 | from fastapi import Depends 5 | 6 | from models.location import Location 7 | from models.reports import Report, ReportSubmittal 8 | from models.validation_error import ValidationError 9 | from services import openweather_service, report_service 10 | 11 | router = fastapi.APIRouter() 12 | 13 | 14 | @router.get('/api/weather/{city}') 15 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'): 16 | try: 17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units) 18 | except ValidationError as ve: 19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code) 20 | except Exception as x: 21 | return fastapi.Response(content=str(x), status_code=500) 22 | 23 | 24 | @router.get('/api/reports', name='all_reports', response_model=List[Report]) 25 | async def reports_get() -> List[Report]: 26 | # await report_service.add_report("A", Location(city="Portland")) 27 | # await report_service.add_report("B", Location(city="NYC")) 28 | return await report_service.get_reports() 29 | 30 | 31 | @router.post('/api/reports', name='add_report', status_code=201, response_model=Report) 32 | async def reports_post(report_submittal: ReportSubmittal) -> Report: 33 | d = report_submittal.description 34 | loc = report_submittal.location 35 | 36 | return await report_service.add_report(d, loc) 37 | -------------------------------------------------------------------------------- /ch07-inbound-data/infrastructure/weather_cache.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, Tuple 3 | 4 | __cache = {} 5 | lifetime_in_hours = 1.0 6 | 7 | 8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]: 9 | key = __create_key(city, state, country, units) 10 | data: dict = __cache.get(key) 11 | if not data: 12 | return None 13 | 14 | last = data['time'] 15 | dt = datetime.datetime.now() - last 16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours: 17 | return data['value'] 18 | 19 | del __cache[key] 20 | return None 21 | 22 | 23 | def set_weather(city: str, state: str, country: str, units: str, value: dict): 24 | key = __create_key(city, state, country, units) 25 | data = { 26 | 'time': datetime.datetime.now(), 27 | 'value': value 28 | } 29 | __cache[key] = data 30 | __clean_out_of_date() 31 | 32 | 33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 34 | if not city or not country or not units: 35 | raise Exception("City, country, and units are required") 36 | 37 | if not state: 38 | state = "" 39 | 40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 41 | 42 | 43 | def __clean_out_of_date(): 44 | for key, data in list(__cache.items()): 45 | dt = datetime.datetime.now() - data.get('time') 46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 47 | del __cache[key] 48 | -------------------------------------------------------------------------------- /ch08-deployment/infrastructure/weather_cache.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, Tuple 3 | 4 | __cache = {} 5 | lifetime_in_hours = 1.0 6 | 7 | 8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]: 9 | key = __create_key(city, state, country, units) 10 | data: dict = __cache.get(key) 11 | if not data: 12 | return None 13 | 14 | last = data['time'] 15 | dt = datetime.datetime.now() - last 16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours: 17 | return data['value'] 18 | 19 | del __cache[key] 20 | return None 21 | 22 | 23 | def set_weather(city: str, state: str, country: str, units: str, value: dict): 24 | key = __create_key(city, state, country, units) 25 | data = { 26 | 'time': datetime.datetime.now(), 27 | 'value': value 28 | } 29 | __cache[key] = data 30 | __clean_out_of_date() 31 | 32 | 33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 34 | if not city or not country or not units: 35 | raise Exception("City, country, and units are required") 36 | 37 | if not state: 38 | state = "" 39 | 40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 41 | 42 | 43 | def __clean_out_of_date(): 44 | for key, data in list(__cache.items()): 45 | dt = datetime.datetime.now() - data.get('time') 46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 47 | del __cache[key] 48 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/infrastructure/weather_cache.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Optional, Tuple 3 | 4 | __cache = {} 5 | lifetime_in_hours = 1.0 6 | 7 | 8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]: 9 | key = __create_key(city, state, country, units) 10 | data: dict = __cache.get(key) 11 | if not data: 12 | return None 13 | 14 | last = data['time'] 15 | dt = datetime.datetime.now() - last 16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours: 17 | return data['value'] 18 | 19 | del __cache[key] 20 | return None 21 | 22 | 23 | def set_weather(city: str, state: str, country: str, units: str, value: dict): 24 | key = __create_key(city, state, country, units) 25 | data = { 26 | 'time': datetime.datetime.now(), 27 | 'value': value 28 | } 29 | __cache[key] = data 30 | __clean_out_of_date() 31 | 32 | 33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 34 | if not city or not country or not units: 35 | raise Exception("City, country, and units are required") 36 | 37 | if not state: 38 | state = "" 39 | 40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 41 | 42 | 43 | def __clean_out_of_date(): 44 | for key, data in list(__cache.items()): 45 | dt = datetime.datetime.now() - data.get('time') 46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 47 | del __cache[key] 48 | -------------------------------------------------------------------------------- /ch07-inbound-data/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | from pathlib import Path 4 | 5 | import fastapi 6 | import uvicorn 7 | from starlette.staticfiles import StaticFiles 8 | 9 | from api import weather_api 10 | from models.location import Location 11 | from services import openweather_service, report_service 12 | from views import home 13 | 14 | api = fastapi.FastAPI() 15 | 16 | 17 | def configure(): 18 | configure_routing() 19 | configure_api_keys() 20 | configure_fake_data() 21 | 22 | 23 | def configure_api_keys(): 24 | file = Path('settings.json').absolute() 25 | if not file.exists(): 26 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json") 27 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json") 28 | 29 | with open('settings.json') as fin: 30 | settings = json.load(fin) 31 | openweather_service.api_key = settings.get('api_key') 32 | 33 | 34 | def configure_routing(): 35 | api.mount('/static', StaticFiles(directory='static'), name='static') 36 | api.include_router(home.router) 37 | api.include_router(weather_api.router) 38 | 39 | 40 | def configure_fake_data(): 41 | # This was added to make it easier to test the weather event reporting 42 | # We have /api/reports but until you submit new data each run, it's missing 43 | # So this will give us something to start from. 44 | loc = Location(city="Portland", state="OR", country="US") 45 | asyncio.run(report_service.add_report("Misty sunrise today, beautiful!", loc)) 46 | asyncio.run(report_service.add_report("Clouds over downtown.", loc)) 47 | 48 | 49 | if __name__ == '__main__': 50 | configure() 51 | uvicorn.run(api, port=8000, host='127.0.0.1') 52 | else: 53 | configure() 54 | -------------------------------------------------------------------------------- /ch08-deployment/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import fastapi 5 | import uvicorn 6 | from starlette.staticfiles import StaticFiles 7 | 8 | from api import weather_api 9 | from services import openweather_service 10 | from views import home 11 | 12 | api = fastapi.FastAPI() 13 | 14 | 15 | def configure(): 16 | configure_routing() 17 | configure_api_keys() 18 | configure_fake_data() 19 | 20 | 21 | def configure_api_keys(): 22 | file = Path('settings.json').absolute() 23 | if not file.exists(): 24 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json") 25 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json") 26 | 27 | with open('settings.json') as fin: 28 | settings = json.load(fin) 29 | openweather_service.api_key = settings.get('api_key') 30 | 31 | 32 | def configure_routing(): 33 | api.mount('/static', StaticFiles(directory='static'), name='static') 34 | api.include_router(home.router) 35 | api.include_router(weather_api.router) 36 | 37 | 38 | def configure_fake_data(): 39 | # This was added to make it easier to test the weather event reporting 40 | # We have /api/reports but until you submit new data each run, it's missing 41 | # So this will give us something to start from. 42 | pass # Doesn't work on Ubuntu under gunicorn 43 | # try: 44 | # loc = Location(city="Portland", state="OR", country="US") 45 | # asyncio.run(report_service.add_report("Misty sunrise today, beautiful!", loc)) 46 | # asyncio.run(report_service.add_report("Clouds over downtown.", loc)) 47 | # except: 48 | # print("NOTICE: Add default data not supported on this system (usually under uvicorn on linux)") 49 | 50 | 51 | if __name__ == '__main__': 52 | configure() 53 | uvicorn.run(api, port=8000, host='127.0.0.1') 54 | else: 55 | configure() 56 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/async_scrape/program.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import datetime 3 | 4 | import httpx 5 | import bs4 6 | from colorama import Fore 7 | 8 | # Older versions of python require calling loop.create_task() rather than on asyncio. 9 | # Make this available more easily. 10 | global loop 11 | 12 | 13 | async def get_html(episode_number: int) -> str: 14 | print(Fore.YELLOW + f"Getting HTML for episode {episode_number}", flush=True) 15 | 16 | url = f'https://talkpython.fm/{episode_number}' 17 | 18 | async with httpx.AsyncClient() as client: 19 | resp = await client.get(url) 20 | resp.raise_for_status() 21 | 22 | return resp.text 23 | 24 | 25 | def get_title(html: str, episode_number: int) -> str: 26 | print(Fore.CYAN + f"Getting TITLE for episode {episode_number}", flush=True) 27 | soup = bs4.BeautifulSoup(html, 'html.parser') 28 | header = soup.select_one('h1') 29 | if not header: 30 | return "MISSING" 31 | 32 | return header.text.strip() 33 | 34 | 35 | def main(): 36 | t0 = datetime.datetime.now() 37 | 38 | global loop 39 | loop = asyncio.get_event_loop() 40 | loop.run_until_complete(get_title_range()) 41 | 42 | dt = datetime.datetime.now() - t0 43 | print(f"Done in {dt.total_seconds():.2f} sec.") 44 | 45 | 46 | async def get_title_range_old_version(): 47 | # Please keep this range pretty small to not DDoS my site. ;) 48 | for n in range(270, 280): 49 | html = await get_html(n) 50 | title = get_title(html, n) 51 | print(Fore.WHITE + f"Title found: {title}", flush=True) 52 | 53 | 54 | async def get_title_range(): 55 | # Please keep this range pretty small to not DDoS my site. ;) 56 | 57 | tasks = [] 58 | for n in range(270, 280): 59 | tasks.append((n, loop.create_task(get_html(n)))) 60 | 61 | for n, t in tasks: 62 | html = await t 63 | title = get_title(html, n) 64 | print(Fore.WHITE + f"Title found: {title}", flush=True) 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /ch04-language-foundations/models/orders_v1.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List 3 | 4 | from dateutil.parser import parse 5 | 6 | order_json = { 7 | 'item_id': '123', 8 | 'created_date': '2002-11-24 12:22', 9 | 'pages_visited': [1, 2, '3'], 10 | 'price': 17.22 11 | } 12 | 13 | 14 | # class Order: 15 | # 16 | # def __init__(self, item_id: int, created_date: datetime.datetime, 17 | # pages_visited: List[int], price: float): 18 | # self.item_id = item_id 19 | # self.created_date = created_date 20 | # self.pages_visited = pages_visited 21 | # self.price = price 22 | # 23 | # def __str__(self): 24 | # return str(self.__dict__) 25 | 26 | class Order: 27 | 28 | def __init__(self, item_id: int, created_date: datetime.datetime, price: float, pages_visited=None): 29 | if pages_visited is None: 30 | pages_visited = [] 31 | 32 | try: 33 | self.item_id = int(item_id) 34 | except ValueError: 35 | raise Exception("Invalid item_id, it must be an integer.") 36 | 37 | try: 38 | self.created_date = parse(created_date) 39 | except: 40 | raise Exception("Invalid created_date, it must be an datetime.") 41 | 42 | try: 43 | self.price = float(price) 44 | except ValueError: 45 | raise Exception("Invalid price, it must be an float.") 46 | 47 | try: 48 | self.pages_visited = [int(p) for p in pages_visited] 49 | except: 50 | raise Exception("Invalid page list, it must be iterable and contain only integers.") 51 | 52 | def __str__(self): 53 | return f'item_id={self.item_id}, created_date={repr(self.created_date)}, ' \ 54 | f'price={self.price}, pages_visited={self.pages_visited}' 55 | 56 | def __eq__(self, other): 57 | return isinstance(other, Order) and self.__dict__ == other.__dict__ 58 | 59 | def __ne__(self, other): 60 | return isinstance(other, Order) and self.__dict__ == other.__dict__ 61 | 62 | 63 | o = Order(**order_json) 64 | print(o) 65 | -------------------------------------------------------------------------------- /ch07-inbound-data/templates/shared/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Weather API 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | {% block content %} 29 |
30 | THIS PAGE HAS NO CONTENT 31 |
32 | {% endblock %} 33 | 34 |
35 |
36 |
37 |
38 | 48 |
49 |
50 | 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /ch08-deployment/templates/shared/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Weather API 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | {% block content %} 29 |
30 | THIS PAGE HAS NO CONTENT 31 |
32 | {% endblock %} 33 | 34 |
35 |
36 |
37 |
38 | 48 |
49 |
50 | 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/templates/shared/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Weather API 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | {% block content %} 29 |
30 | THIS PAGE HAS NO CONTENT 31 |
32 | {% endblock %} 33 | 34 |
35 |
36 |
37 |
38 | 48 |
49 |
50 | 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/templates/shared/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Weather API 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | {% block content %} 29 |
30 | THIS PAGE HAS NO CONTENT 31 |
32 | {% endblock %} 33 | 34 |
35 |
36 |
37 |
38 | 48 |
49 |
50 | 53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "shared/layout.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 |

Weather Service A RESTful weather service 8 |

9 | 10 |
11 |
12 |
13 |

14 | The Talk Python weather service.
15 |
16 | Endpoints 17 |

49 |

50 | 51 | 52 | 53 | {% endblock %} -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "shared/layout.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 |

Weather Service A RESTful weather service 8 |

9 | 10 |
11 |
12 |
13 |

14 | The Talk Python weather service.
15 |
16 | Endpoints 17 |

49 |

50 | 51 | 52 | 53 | {% endblock %} -------------------------------------------------------------------------------- /ch08-deployment/server/scripts/server_setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Consider running these two commands separately 4 | # Do a reboot before continuing. 5 | apt update 6 | apt upgrade -y 7 | 8 | apt install zsh 9 | sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" 10 | 11 | # Install some OS dependencies: 12 | sudo apt-get install -y -q build-essential git unzip zip nload tree 13 | sudo apt-get install -y -q python3-pip python3-dev python3-venv 14 | 15 | # Stop the hackers 16 | sudo apt install fail2ban -y 17 | 18 | ufw allow 22 19 | ufw allow 80 20 | ufw allow 443 21 | ufw enable 22 | 23 | 24 | apt install acl -y 25 | useradd -M apiuser 26 | usermod -L apiuser 27 | setfacl -m u:apiuser:rwx /apps/logs/weather_api 28 | 29 | 30 | # Web app file structure 31 | mkdir /apps 32 | chmod 777 /apps 33 | mkdir /apps/logs 34 | mkdir /apps/logs/weather_api 35 | mkdir /apps/logs/weather_api/app_log 36 | # chmod 777 /apps/logs/weather_api 37 | cd /apps 38 | 39 | # Create a virtual env for the app. 40 | cd /apps 41 | python3 -m venv venv 42 | source /apps/venv/bin/activate 43 | pip install --upgrade pip setuptools wheel 44 | pip install --upgrade httpie glances 45 | pip install --upgrade gunicorn uvloop httptools 46 | 47 | # clone the repo: 48 | cd /apps 49 | git clone https://github.com/talkpython/modern-apis-with-fastapi app_repo 50 | 51 | # Setup the web app: 52 | cd /apps/app_repo/ch08-deployment 53 | pip install -r requirements.txt 54 | 55 | # Copy and enable the daemon 56 | cp /apps/app_repo/ch08-deployment/server/units/weather.service /etc/systemd/system/ 57 | 58 | systemctl start weather 59 | systemctl status weather 60 | systemctl enable weather 61 | 62 | # Setup the public facing server (NGINX) 63 | apt install nginx 64 | 65 | # CAREFUL HERE. If you are using default, maybe skip this 66 | rm /etc/nginx/sites-enabled/default 67 | 68 | cp /apps/app_repo/ch08-deployment/server/nginx/weather.nginx /etc/nginx/sites-enabled/ 69 | update-rc.d nginx enable 70 | service nginx restart 71 | 72 | 73 | # Optionally add SSL support via Let's Encrypt: 74 | # https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04 75 | 76 | add-apt-repository ppa:certbot/certbot 77 | apt install python3-certbot-nginx 78 | certbot --nginx -d weatherapi.talkpython.com 79 | -------------------------------------------------------------------------------- /ch07-inbound-data/services/openweather_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | import httpx 3 | from httpx import Response 4 | 5 | from infrastructure import weather_cache 6 | from models.validation_error import ValidationError 7 | 8 | api_key: Optional[str] = None 9 | 10 | 11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict: 12 | city, state, country, units = validate_units(city, state, country, units) 13 | 14 | if forecast := weather_cache.get_weather(city, state, country, units): 15 | return forecast 16 | 17 | if state: 18 | q = f'{city},{state},{country}' 19 | else: 20 | q = f'{city},{country}' 21 | 22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}' 23 | 24 | async with httpx.AsyncClient() as client: 25 | resp: Response = await client.get(url) 26 | if resp.status_code != 200: 27 | raise ValidationError(resp.text, status_code=resp.status_code) 28 | 29 | data = resp.json() 30 | forecast = data['main'] 31 | 32 | weather_cache.set_weather(city, state, country, units, forecast) 33 | 34 | return forecast 35 | 36 | 37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \ 38 | Tuple[str, Optional[str], str, str]: 39 | city = city.lower().strip() 40 | if not country: 41 | country = "us" 42 | else: 43 | country = country.lower().strip() 44 | 45 | if len(country) != 2: 46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB." 47 | raise ValidationError(status_code=400, error_msg=error) 48 | 49 | if state: 50 | state = state.strip().lower() 51 | 52 | if state and len(state) != 2: 53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)." 54 | raise ValidationError(status_code=400, error_msg=error) 55 | 56 | if units: 57 | units = units.strip().lower() 58 | 59 | valid_units = {'standard', 'metric', 'imperial'} 60 | if units not in valid_units: 61 | error = f"Invalid units '{units}', it must be one of {valid_units}." 62 | raise ValidationError(status_code=400, error_msg=error) 63 | 64 | return city, state, country, units 65 | -------------------------------------------------------------------------------- /ch08-deployment/services/openweather_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | import httpx 3 | from httpx import Response 4 | 5 | from infrastructure import weather_cache 6 | from models.validation_error import ValidationError 7 | 8 | api_key: Optional[str] = None 9 | 10 | 11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict: 12 | city, state, country, units = validate_units(city, state, country, units) 13 | 14 | if forecast := weather_cache.get_weather(city, state, country, units): 15 | return forecast 16 | 17 | if state: 18 | q = f'{city},{state},{country}' 19 | else: 20 | q = f'{city},{country}' 21 | 22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}' 23 | 24 | async with httpx.AsyncClient() as client: 25 | resp: Response = await client.get(url) 26 | if resp.status_code != 200: 27 | raise ValidationError(resp.text, status_code=resp.status_code) 28 | 29 | data = resp.json() 30 | forecast = data['main'] 31 | 32 | weather_cache.set_weather(city, state, country, units, forecast) 33 | 34 | return forecast 35 | 36 | 37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \ 38 | Tuple[str, Optional[str], str, str]: 39 | city = city.lower().strip() 40 | if not country: 41 | country = "us" 42 | else: 43 | country = country.lower().strip() 44 | 45 | if len(country) != 2: 46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB." 47 | raise ValidationError(status_code=400, error_msg=error) 48 | 49 | if state: 50 | state = state.strip().lower() 51 | 52 | if state and len(state) != 2: 53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)." 54 | raise ValidationError(status_code=400, error_msg=error) 55 | 56 | if units: 57 | units = units.strip().lower() 58 | 59 | valid_units = {'standard', 'metric', 'imperial'} 60 | if units not in valid_units: 61 | error = f"Invalid units '{units}', it must be one of {valid_units}." 62 | raise ValidationError(status_code=400, error_msg=error) 63 | 64 | return city, state, country, units 65 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/services/openweather_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Tuple 2 | import httpx 3 | from httpx import Response 4 | 5 | from infrastructure import weather_cache 6 | from models.validation_error import ValidationError 7 | 8 | api_key: Optional[str] = None 9 | 10 | 11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict: 12 | city, state, country, units = validate_units(city, state, country, units) 13 | 14 | if forecast := weather_cache.get_weather(city, state, country, units): 15 | return forecast 16 | 17 | if state: 18 | q = f'{city},{state},{country}' 19 | else: 20 | q = f'{city},{country}' 21 | 22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}' 23 | 24 | async with httpx.AsyncClient() as client: 25 | resp: Response = await client.get(url) 26 | if resp.status_code != 200: 27 | raise ValidationError(resp.text, status_code=resp.status_code) 28 | 29 | data = resp.json() 30 | forecast = data['main'] 31 | 32 | weather_cache.set_weather(city, state, country, units, forecast) 33 | 34 | return forecast 35 | 36 | 37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \ 38 | Tuple[str, Optional[str], str, str]: 39 | city = city.lower().strip() 40 | if not country: 41 | country = "us" 42 | else: 43 | country = country.lower().strip() 44 | 45 | if len(country) != 2: 46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB." 47 | raise ValidationError(status_code=400, error_msg=error) 48 | 49 | if state: 50 | state = state.strip().lower() 51 | 52 | if state and len(state) != 2: 53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)." 54 | raise ValidationError(status_code=400, error_msg=error) 55 | 56 | if units: 57 | units = units.strip().lower() 58 | 59 | valid_units = {'standard', 'metric', 'imperial'} 60 | if units not in valid_units: 61 | error = f"Invalid units '{units}', it must be one of {valid_units}." 62 | raise ValidationError(status_code=400, error_msg=error) 63 | 64 | return city, state, country, units 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | settings.json 131 | -------------------------------------------------------------------------------- /ch07-inbound-data/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "shared/layout.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 |

Weather Service A RESTful weather service 8 |

9 | 10 |
11 |
12 |
13 |

14 | The Talk Python weather service.
15 |
16 | Endpoints 17 |

49 |

50 | 51 | {% if events %} 52 |
53 |

Recent weather events

54 | 59 |
60 | {% endif %} 61 | 62 | 63 | 64 | {% endblock %} -------------------------------------------------------------------------------- /ch08-deployment/templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "shared/layout.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 |

Weather Service A RESTful weather service 8 |

9 | 10 |
11 |
12 |
13 |

14 | The Talk Python weather service.
15 |
16 | Endpoints 17 |

49 |

50 | 51 | {% if events %} 52 |
53 |

Recent weather events

54 | 59 |
60 | {% endif %} 61 | 62 | 63 | 64 | {% endblock %} -------------------------------------------------------------------------------- /ch08-deployment/static/css/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | 3 | body { 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-weight: 300; 6 | color: black; 7 | background: white; 8 | padding-left: 20px; 9 | padding-right: 20px; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 { 18 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight: 300; 20 | } 21 | 22 | p { 23 | font-weight: 300; 24 | } 25 | 26 | .font-normal { 27 | font-weight: 400; 28 | } 29 | 30 | .font-semi-bold { 31 | font-weight: 600; 32 | } 33 | 34 | .font-bold { 35 | font-weight: 700; 36 | } 37 | 38 | .starter-template { 39 | margin-top: 25px; 40 | } 41 | 42 | .starter-template .content { 43 | margin-left: 10px; 44 | } 45 | 46 | .starter-template .content h1 { 47 | margin-top: 10px; 48 | font-size: 60px; 49 | } 50 | 51 | .starter-template .content h1 .smaller { 52 | font-size: 40px; 53 | } 54 | 55 | .starter-template .content .lead { 56 | font-size: 25px; 57 | } 58 | 59 | .starter-template .links { 60 | float: right; 61 | right: 0; 62 | margin-top: 125px; 63 | } 64 | 65 | .starter-template .links ul { 66 | display: block; 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | .starter-template .links ul li { 72 | list-style: none; 73 | display: inline; 74 | margin: 0 10px; 75 | } 76 | 77 | .starter-template .links ul li:first-child { 78 | margin-left: 0; 79 | } 80 | 81 | .starter-template .links ul li:last-child { 82 | margin-right: 0; 83 | } 84 | 85 | .starter-template .links ul li.current-version { 86 | font-weight: 400; 87 | } 88 | 89 | .starter-template .links ul li a, a { 90 | text-decoration: underline; 91 | } 92 | 93 | .starter-template .links ul li a:hover, a:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | .starter-template .links ul li .icon-muted { 98 | margin-right: 5px; 99 | } 100 | 101 | .starter-template .copyright { 102 | margin-top: 10px; 103 | font-size: 0.9em; 104 | text-transform: lowercase; 105 | float: right; 106 | right: 0; 107 | } 108 | 109 | @media (max-width: 1199px) { 110 | .starter-template .content h1 { 111 | font-size: 45px; 112 | } 113 | 114 | .starter-template .content h1 .smaller { 115 | font-size: 30px; 116 | } 117 | 118 | .starter-template .content .lead { 119 | font-size: 20px; 120 | } 121 | } 122 | 123 | @media (max-width: 991px) { 124 | .starter-template { 125 | margin-top: 0; 126 | } 127 | 128 | .starter-template .logo { 129 | margin: 40px auto; 130 | } 131 | 132 | .starter-template .content { 133 | margin-left: 0; 134 | text-align: center; 135 | } 136 | 137 | .starter-template .content h1 { 138 | margin-bottom: 20px; 139 | } 140 | 141 | .starter-template .links { 142 | float: none; 143 | text-align: center; 144 | margin-top: 60px; 145 | } 146 | 147 | .starter-template .copyright { 148 | float: none; 149 | text-align: center; 150 | } 151 | } 152 | 153 | @media (max-width: 767px) { 154 | .starter-template .content h1 .smaller { 155 | font-size: 25px; 156 | display: block; 157 | } 158 | 159 | .starter-template .content .lead { 160 | font-size: 16px; 161 | } 162 | 163 | .starter-template .links { 164 | margin-top: 40px; 165 | } 166 | 167 | .starter-template .links ul li { 168 | display: block; 169 | margin: 0; 170 | } 171 | 172 | .starter-template .links ul li .icon-muted { 173 | display: none; 174 | } 175 | 176 | .starter-template .copyright { 177 | margin-top: 20px; 178 | } 179 | } 180 | 181 | 182 | .disclaimer { 183 | margin-top: 20px; 184 | font-style: italic; 185 | 186 | } -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/css/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | 3 | body { 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-weight: 300; 6 | color: black; 7 | background: white; 8 | padding-left: 20px; 9 | padding-right: 20px; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 { 18 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight: 300; 20 | } 21 | 22 | p { 23 | font-weight: 300; 24 | } 25 | 26 | .font-normal { 27 | font-weight: 400; 28 | } 29 | 30 | .font-semi-bold { 31 | font-weight: 600; 32 | } 33 | 34 | .font-bold { 35 | font-weight: 700; 36 | } 37 | 38 | .starter-template { 39 | margin-top: 25px; 40 | } 41 | 42 | .starter-template .content { 43 | margin-left: 10px; 44 | } 45 | 46 | .starter-template .content h1 { 47 | margin-top: 10px; 48 | font-size: 60px; 49 | } 50 | 51 | .starter-template .content h1 .smaller { 52 | font-size: 40px; 53 | } 54 | 55 | .starter-template .content .lead { 56 | font-size: 25px; 57 | } 58 | 59 | .starter-template .links { 60 | float: right; 61 | right: 0; 62 | margin-top: 125px; 63 | } 64 | 65 | .starter-template .links ul { 66 | display: block; 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | .starter-template .links ul li { 72 | list-style: none; 73 | display: inline; 74 | margin: 0 10px; 75 | } 76 | 77 | .starter-template .links ul li:first-child { 78 | margin-left: 0; 79 | } 80 | 81 | .starter-template .links ul li:last-child { 82 | margin-right: 0; 83 | } 84 | 85 | .starter-template .links ul li.current-version { 86 | font-weight: 400; 87 | } 88 | 89 | .starter-template .links ul li a, a { 90 | text-decoration: underline; 91 | } 92 | 93 | .starter-template .links ul li a:hover, a:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | .starter-template .links ul li .icon-muted { 98 | margin-right: 5px; 99 | } 100 | 101 | .starter-template .copyright { 102 | margin-top: 10px; 103 | font-size: 0.9em; 104 | text-transform: lowercase; 105 | float: right; 106 | right: 0; 107 | } 108 | 109 | @media (max-width: 1199px) { 110 | .starter-template .content h1 { 111 | font-size: 45px; 112 | } 113 | 114 | .starter-template .content h1 .smaller { 115 | font-size: 30px; 116 | } 117 | 118 | .starter-template .content .lead { 119 | font-size: 20px; 120 | } 121 | } 122 | 123 | @media (max-width: 991px) { 124 | .starter-template { 125 | margin-top: 0; 126 | } 127 | 128 | .starter-template .logo { 129 | margin: 40px auto; 130 | } 131 | 132 | .starter-template .content { 133 | margin-left: 0; 134 | text-align: center; 135 | } 136 | 137 | .starter-template .content h1 { 138 | margin-bottom: 20px; 139 | } 140 | 141 | .starter-template .links { 142 | float: none; 143 | text-align: center; 144 | margin-top: 60px; 145 | } 146 | 147 | .starter-template .copyright { 148 | float: none; 149 | text-align: center; 150 | } 151 | } 152 | 153 | @media (max-width: 767px) { 154 | .starter-template .content h1 .smaller { 155 | font-size: 25px; 156 | display: block; 157 | } 158 | 159 | .starter-template .content .lead { 160 | font-size: 16px; 161 | } 162 | 163 | .starter-template .links { 164 | margin-top: 40px; 165 | } 166 | 167 | .starter-template .links ul li { 168 | display: block; 169 | margin: 0; 170 | } 171 | 172 | .starter-template .links ul li .icon-muted { 173 | display: none; 174 | } 175 | 176 | .starter-template .copyright { 177 | margin-top: 20px; 178 | } 179 | } 180 | 181 | 182 | .disclaimer { 183 | margin-top: 20px; 184 | font-style: italic; 185 | 186 | } -------------------------------------------------------------------------------- /ch07-inbound-data/static/css/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | 3 | body { 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-weight: 300; 6 | color: black; 7 | background: white; 8 | padding-left: 20px; 9 | padding-right: 20px; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 { 18 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight: 300; 20 | } 21 | 22 | p { 23 | font-weight: 300; 24 | } 25 | 26 | .font-normal { 27 | font-weight: 400; 28 | } 29 | 30 | .font-semi-bold { 31 | font-weight: 600; 32 | } 33 | 34 | .font-bold { 35 | font-weight: 700; 36 | } 37 | 38 | .starter-template { 39 | margin-top: 25px; 40 | } 41 | 42 | .starter-template .content { 43 | margin-left: 10px; 44 | } 45 | 46 | .starter-template .content h1 { 47 | margin-top: 10px; 48 | font-size: 60px; 49 | } 50 | 51 | .starter-template .content h1 .smaller { 52 | font-size: 40px; 53 | } 54 | 55 | .starter-template .content .lead { 56 | font-size: 25px; 57 | } 58 | 59 | .starter-template .links { 60 | float: right; 61 | right: 0; 62 | margin-top: 125px; 63 | } 64 | 65 | .starter-template .links ul { 66 | display: block; 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | .starter-template .links ul li { 72 | list-style: none; 73 | display: inline; 74 | margin: 0 10px; 75 | } 76 | 77 | .starter-template .links ul li:first-child { 78 | margin-left: 0; 79 | } 80 | 81 | .starter-template .links ul li:last-child { 82 | margin-right: 0; 83 | } 84 | 85 | .starter-template .links ul li.current-version { 86 | font-weight: 400; 87 | } 88 | 89 | .starter-template .links ul li a, a { 90 | text-decoration: underline; 91 | } 92 | 93 | .starter-template .links ul li a:hover, a:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | .starter-template .links ul li .icon-muted { 98 | margin-right: 5px; 99 | } 100 | 101 | .starter-template .copyright { 102 | margin-top: 10px; 103 | font-size: 0.9em; 104 | text-transform: lowercase; 105 | float: right; 106 | right: 0; 107 | } 108 | 109 | @media (max-width: 1199px) { 110 | .starter-template .content h1 { 111 | font-size: 45px; 112 | } 113 | 114 | .starter-template .content h1 .smaller { 115 | font-size: 30px; 116 | } 117 | 118 | .starter-template .content .lead { 119 | font-size: 20px; 120 | } 121 | } 122 | 123 | @media (max-width: 991px) { 124 | .starter-template { 125 | margin-top: 0; 126 | } 127 | 128 | .starter-template .logo { 129 | margin: 40px auto; 130 | } 131 | 132 | .starter-template .content { 133 | margin-left: 0; 134 | text-align: center; 135 | } 136 | 137 | .starter-template .content h1 { 138 | margin-bottom: 20px; 139 | } 140 | 141 | .starter-template .links { 142 | float: none; 143 | text-align: center; 144 | margin-top: 60px; 145 | } 146 | 147 | .starter-template .copyright { 148 | float: none; 149 | text-align: center; 150 | } 151 | } 152 | 153 | @media (max-width: 767px) { 154 | .starter-template .content h1 .smaller { 155 | font-size: 25px; 156 | display: block; 157 | } 158 | 159 | .starter-template .content .lead { 160 | font-size: 16px; 161 | } 162 | 163 | .starter-template .links { 164 | margin-top: 40px; 165 | } 166 | 167 | .starter-template .links ul li { 168 | display: block; 169 | margin: 0; 170 | } 171 | 172 | .starter-template .links ul li .icon-muted { 173 | display: none; 174 | } 175 | 176 | .starter-template .copyright { 177 | margin-top: 20px; 178 | } 179 | } 180 | 181 | 182 | .disclaimer { 183 | margin-top: 20px; 184 | font-style: italic; 185 | 186 | } -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/static/css/theme.css: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); 2 | 3 | body { 4 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | font-weight: 300; 6 | color: black; 7 | background: white; 8 | padding-left: 20px; 9 | padding-right: 20px; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3, 15 | h4, 16 | h5, 17 | h6 { 18 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | font-weight: 300; 20 | } 21 | 22 | p { 23 | font-weight: 300; 24 | } 25 | 26 | .font-normal { 27 | font-weight: 400; 28 | } 29 | 30 | .font-semi-bold { 31 | font-weight: 600; 32 | } 33 | 34 | .font-bold { 35 | font-weight: 700; 36 | } 37 | 38 | .starter-template { 39 | margin-top: 25px; 40 | } 41 | 42 | .starter-template .content { 43 | margin-left: 10px; 44 | } 45 | 46 | .starter-template .content h1 { 47 | margin-top: 10px; 48 | font-size: 60px; 49 | } 50 | 51 | .starter-template .content h1 .smaller { 52 | font-size: 40px; 53 | } 54 | 55 | .starter-template .content .lead { 56 | font-size: 25px; 57 | } 58 | 59 | .starter-template .links { 60 | float: right; 61 | right: 0; 62 | margin-top: 125px; 63 | } 64 | 65 | .starter-template .links ul { 66 | display: block; 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | .starter-template .links ul li { 72 | list-style: none; 73 | display: inline; 74 | margin: 0 10px; 75 | } 76 | 77 | .starter-template .links ul li:first-child { 78 | margin-left: 0; 79 | } 80 | 81 | .starter-template .links ul li:last-child { 82 | margin-right: 0; 83 | } 84 | 85 | .starter-template .links ul li.current-version { 86 | font-weight: 400; 87 | } 88 | 89 | .starter-template .links ul li a, a { 90 | text-decoration: underline; 91 | } 92 | 93 | .starter-template .links ul li a:hover, a:hover { 94 | text-decoration: underline; 95 | } 96 | 97 | .starter-template .links ul li .icon-muted { 98 | margin-right: 5px; 99 | } 100 | 101 | .starter-template .copyright { 102 | margin-top: 10px; 103 | font-size: 0.9em; 104 | text-transform: lowercase; 105 | float: right; 106 | right: 0; 107 | } 108 | 109 | @media (max-width: 1199px) { 110 | .starter-template .content h1 { 111 | font-size: 45px; 112 | } 113 | 114 | .starter-template .content h1 .smaller { 115 | font-size: 30px; 116 | } 117 | 118 | .starter-template .content .lead { 119 | font-size: 20px; 120 | } 121 | } 122 | 123 | @media (max-width: 991px) { 124 | .starter-template { 125 | margin-top: 0; 126 | } 127 | 128 | .starter-template .logo { 129 | margin: 40px auto; 130 | } 131 | 132 | .starter-template .content { 133 | margin-left: 0; 134 | text-align: center; 135 | } 136 | 137 | .starter-template .content h1 { 138 | margin-bottom: 20px; 139 | } 140 | 141 | .starter-template .links { 142 | float: none; 143 | text-align: center; 144 | margin-top: 60px; 145 | } 146 | 147 | .starter-template .copyright { 148 | float: none; 149 | text-align: center; 150 | } 151 | } 152 | 153 | @media (max-width: 767px) { 154 | .starter-template .content h1 .smaller { 155 | font-size: 25px; 156 | display: block; 157 | } 158 | 159 | .starter-template .content .lead { 160 | font-size: 16px; 161 | } 162 | 163 | .starter-template .links { 164 | margin-top: 40px; 165 | } 166 | 167 | .starter-template .links ul li { 168 | display: block; 169 | margin: 0; 170 | } 171 | 172 | .starter-template .links ul li .icon-muted { 173 | display: none; 174 | } 175 | 176 | .starter-template .copyright { 177 | margin-top: 20px; 178 | } 179 | } 180 | 181 | 182 | .disclaimer { 183 | margin-top: 20px; 184 | font-style: italic; 185 | 186 | } --------------------------------------------------------------------------------