├── ch08-deployment ├── .idea │ ├── .name │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ ├── webResources.xml │ └── FastAPI Course [ch08-deployment].iml ├── requirements.piptools ├── 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 ├── requirements.txt ├── main.py └── templates │ ├── shared │ └── layout.html │ └── home │ └── index.html ├── ch03-first-api ├── requirements.piptools ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── misc.xml │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── modules.xml │ └── ch03-first-api.iml ├── requirements.txt └── main.py ├── ch04-language-foundations ├── async │ ├── async_scrape │ │ ├── requirements.piptools │ │ ├── requirements.txt │ │ └── program.py │ └── sync_scrape │ │ ├── requirements.piptools │ │ ├── requirements.txt │ │ └── program.py ├── requirements.piptools ├── .idea │ ├── codeStyles │ │ └── codeStyleConfig.xml │ ├── vcs.xml │ ├── .gitignore │ ├── inspectionProfiles │ │ └── profiles_settings.xml │ ├── misc.xml │ ├── modules.xml │ └── ch04-language-foundations.iml ├── asgi │ └── asgi_summary.py ├── models │ ├── orders_v2.py │ └── orders_v1.py ├── types │ ├── no_types_program.py │ └── types_program.py └── requirements.txt ├── ch05-a-realistic-api ├── requirements.piptools ├── 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 ├── requirements.txt ├── main.py └── templates │ ├── shared │ └── layout.html │ └── home │ └── index.html ├── ch06-error-handling-and-perf ├── requirements.piptools ├── 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 ├── requirements.txt ├── main.py ├── infrastructure │ └── weather_cache.py ├── templates │ ├── shared │ │ └── layout.html │ └── home │ │ └── index.html └── services │ └── openweather_service.py ├── ch07-inbound-data ├── requirements.piptools ├── 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 ├── requirements.txt ├── infrastructure │ └── weather_cache.py ├── templates │ ├── shared │ │ └── layout.html │ └── home │ │ └── index.html └── main.py ├── readme_resources └── fastapi-modern.png ├── requirements.piptools ├── .idea └── ruff.xml ├── ruff.toml ├── requirements.txt ├── .gitignore └── README.md /ch08-deployment/.idea/.name: -------------------------------------------------------------------------------- 1 | FastAPI Course [ch08-deployment] -------------------------------------------------------------------------------- /ch03-first-api/requirements.piptools: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | 4 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/async_scrape/requirements.piptools: -------------------------------------------------------------------------------- 1 | bs4 2 | colorama 3 | httpx 4 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/requirements.piptools: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/sync_scrape/requirements.piptools: -------------------------------------------------------------------------------- 1 | requests 2 | bs4 3 | colorama 4 | 5 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/requirements.piptools: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | -------------------------------------------------------------------------------- /ch07-inbound-data/requirements.piptools: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | requests 7 | -------------------------------------------------------------------------------- /ch04-language-foundations/requirements.piptools: -------------------------------------------------------------------------------- 1 | requests 2 | httpx 3 | bs4 4 | colorama 5 | python-dateutil 6 | pydantic 7 | -------------------------------------------------------------------------------- /ch08-deployment/requirements.piptools: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | httpx 4 | jinja2 5 | aiofiles 6 | requests 7 | uvloop 8 | httptools 9 | -------------------------------------------------------------------------------- /ch08-deployment/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch08-deployment/static/img/cloud.png -------------------------------------------------------------------------------- /readme_resources/fastapi-modern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/readme_resources/fastapi-modern.png -------------------------------------------------------------------------------- /ch07-inbound-data/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch07-inbound-data/static/img/cloud.png -------------------------------------------------------------------------------- /ch08-deployment/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch08-deployment/static/img/favicon.ico -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch05-a-realistic-api/static/img/cloud.png -------------------------------------------------------------------------------- /ch07-inbound-data/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch07-inbound-data/static/img/favicon.ico -------------------------------------------------------------------------------- /ch05-a-realistic-api/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/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/talkpython/modern-apis-with-fastapi/HEAD/ch06-error-handling-and-perf/static/img/cloud.png -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/talkpython/modern-apis-with-fastapi/HEAD/ch06-error-handling-and-perf/static/img/favicon.ico -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /requirements.piptools: -------------------------------------------------------------------------------- 1 | ##### foundations ##### 2 | requests 3 | httpx 4 | bs4 5 | colorama 6 | 7 | ##### fastapi ##### 8 | fastapi 9 | uvicorn 10 | httpx 11 | jinja2 12 | aiofiles 13 | requests 14 | uvloop 15 | httptools 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/ruff.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ch08-deployment/.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 | -------------------------------------------------------------------------------- /ch08-deployment/.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ch04-language-foundations/models/orders_v2.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import List, Optional 3 | 4 | from pydantic import BaseModel 5 | 6 | order_json = {'item_id': '123', 'created_date': '2002-11-24 12:22', 'pages_visited': [1, 2, '3'], 'price': 17.22} 7 | 8 | 9 | class Order(BaseModel): 10 | item_id: int 11 | created_date: Optional[datetime.datetime] 12 | pages_visited: List[int] = [] 13 | price: float 14 | 15 | 16 | o = Order(**order_json) 17 | print(o) 18 | 19 | 20 | # Default for JSON post 21 | # Can be done for others with mods. 22 | # noinspection PyUnusedLocal 23 | def order_api(order: Order): 24 | pass 25 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/sync_scrape/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | beautifulsoup4==4.12.2 8 | # via bs4 9 | bs4==0.0.1 10 | # via -r requirements.piptools 11 | certifi==2023.11.17 12 | # via requests 13 | charset-normalizer==3.3.2 14 | # via requests 15 | colorama==0.4.6 16 | # via -r requirements.piptools 17 | idna==3.6 18 | # via requests 19 | requests==2.31.0 20 | # via -r requirements.piptools 21 | soupsieve==2.5 22 | # via beautifulsoup4 23 | urllib3==2.1.0 24 | # via requests 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | try: 16 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units) 17 | except ValidationError as ve: 18 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code) 19 | except Exception as x: 20 | return fastapi.Response(content=str(x), status_code=500) 21 | -------------------------------------------------------------------------------- /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 | # Would be an async call here. 13 | return list(__reports) 14 | 15 | 16 | async def add_report(description: str, location: Location) -> Report: 17 | now = datetime.datetime.now() 18 | report = Report(id=str(uuid.uuid4()), location=location, description=description, created_date=now) 19 | 20 | # Simulate saving to the DB. 21 | # Would be an async call here. 22 | __reports.append(report) 23 | 24 | __reports.sort(key=lambda r: r.created_date, reverse=True) 25 | 26 | return report 27 | -------------------------------------------------------------------------------- /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 | # Would be an async call here. 13 | return list(__reports) 14 | 15 | 16 | async def add_report(description: str, location: Location) -> Report: 17 | now = datetime.datetime.now() 18 | report = Report(id=str(uuid.uuid4()), location=location, description=description, created_date=now) 19 | 20 | # Simulate saving to the DB. 21 | # Would be an async call here. 22 | __reports.append(report) 23 | 24 | __reports.sort(key=lambda r: r.created_date, reverse=True) 25 | 26 | return report 27 | -------------------------------------------------------------------------------- /ch04-language-foundations/async/async_scrape/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | anyio==3.7.1 8 | # via httpx 9 | beautifulsoup4==4.12.2 10 | # via bs4 11 | bs4==0.0.1 12 | # via -r requirements.piptools 13 | certifi==2023.11.17 14 | # via 15 | # httpcore 16 | # httpx 17 | colorama==0.4.6 18 | # via -r requirements.piptools 19 | h11==0.14.0 20 | # via httpcore 21 | httpcore==1.0.2 22 | # via httpx 23 | httpx==0.25.2 24 | # via -r requirements.piptools 25 | idna==3.6 26 | # via 27 | # anyio 28 | # httpx 29 | sniffio==1.3.0 30 | # via 31 | # anyio 32 | # httpx 33 | soupsieve==2.5 34 | # via beautifulsoup4 35 | -------------------------------------------------------------------------------- /ch03-first-api/.idea/ch03-first-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /ch03-first-api/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | annotated-types==0.6.0 8 | # via pydantic 9 | anyio==3.7.1 10 | # via 11 | # fastapi 12 | # starlette 13 | click==8.1.7 14 | # via uvicorn 15 | fastapi==0.105.0 16 | # via -r requirements.piptools 17 | h11==0.14.0 18 | # via uvicorn 19 | idna==3.6 20 | # via anyio 21 | pydantic==2.5.2 22 | # via fastapi 23 | pydantic-core==2.14.5 24 | # via pydantic 25 | sniffio==1.3.0 26 | # via anyio 27 | starlette==0.27.0 28 | # via fastapi 29 | typing-extensions==4.9.0 30 | # via 31 | # fastapi 32 | # pydantic 33 | # pydantic-core 34 | uvicorn==0.24.0.post1 35 | # via -r requirements.piptools 36 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/.idea/ch05-a-realistic-api.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 20 | -------------------------------------------------------------------------------- /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/FastAPI Course [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 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # [ruff] 2 | line-length = 120 3 | format.quote-style = "single" 4 | 5 | # Enable Pyflakes `E` and `F` codes by default. 6 | select = ["E", "F"] 7 | ignore = [] 8 | 9 | # Exclude a variety of commonly ignored directories. 10 | exclude = [ 11 | ".bzr", 12 | ".direnv", 13 | ".eggs", 14 | ".git", 15 | ".hg", 16 | ".mypy_cache", 17 | ".nox", 18 | ".pants.d", 19 | ".ruff_cache", 20 | ".svn", 21 | ".tox", 22 | "__pypackages__", 23 | "_build", 24 | "buck-out", 25 | "build", 26 | "dist", 27 | "node_modules", 28 | ".env", 29 | ".venv", 30 | "venv", 31 | ] 32 | per-file-ignores = {} 33 | 34 | # Allow unused variables when underscore-prefixed. 35 | # dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 36 | 37 | # Assume Python 3.11. 38 | target-version = "py311" 39 | 40 | #[tool.ruff.mccabe] 41 | ## Unlike Flake8, default to a complexity level of 10. 42 | mccabe.max-complexity = 10 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "" 14 | '

Welcome to the API

' 15 | '
' 16 | "Try it: /api/calculate?x=7&y=11" 17 | '
' 18 | '' 19 | '' 20 | ) 21 | 22 | return fastapi.responses.HTMLResponse(content=body) 23 | 24 | 25 | @api.get('/api/calculate') 26 | def calculate(x: int, y: int, z: Optional[int] = None): 27 | if z == 0: 28 | return fastapi.responses.JSONResponse(content={'error': 'ERROR: Z cannot be zero.'}, status_code=400) 29 | 30 | value = x + y 31 | 32 | if z is not None: 33 | value /= z 34 | 35 | return {'x': x, 'y': y, 'z': z, 'value': value} 36 | 37 | 38 | # uvicorn was updated, and it's type definitions don't match FastAPI, 39 | # but the server and code still work fine. So ignore PyCharm's warning: 40 | # noinspection PyTypeChecker 41 | uvicorn.run(api, port=8000, host='127.0.0.1') 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 = {'description': desc, 'location': {'city': city}} 22 | 23 | url = 'http://127.0.0.1:8000/api/reports' 24 | resp = requests.post(url, json=data) 25 | resp.raise_for_status() 26 | 27 | result = resp.json() 28 | print(f"Reported new event: {result.get('id')}") 29 | 30 | 31 | def see_events(): 32 | url = 'http://127.0.0.1:8000/api/reports' 33 | resp = requests.get(url) 34 | resp.raise_for_status() 35 | 36 | data = resp.json() 37 | for r in data: 38 | print(f"{r.get('location').get('city')} has {r.get('description')}") 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /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 = {'description': desc, 'location': {'city': city}} 22 | 23 | url = 'http://127.0.0.1:8000/api/reports' 24 | resp = requests.post(url, json=data) 25 | resp.raise_for_status() 26 | 27 | result = resp.json() 28 | print(f"Reported new event: {result.get('id')}") 29 | 30 | 31 | def see_events(): 32 | url = 'http://127.0.0.1:8000/api/reports' 33 | resp = requests.get(url) 34 | resp.raise_for_status() 35 | 36 | data = resp.json() 37 | for r in data: 38 | print(f"{r.get('location').get('city')} has {r.get('description')}") 39 | 40 | 41 | if __name__ == '__main__': 42 | main() 43 | -------------------------------------------------------------------------------- /ch05-a-realistic-api/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | aiofiles==23.2.1 8 | # via -r requirements.piptools 9 | annotated-types==0.6.0 10 | # via pydantic 11 | anyio==3.7.1 12 | # via 13 | # fastapi 14 | # httpx 15 | # starlette 16 | certifi==2023.11.17 17 | # via 18 | # httpcore 19 | # httpx 20 | click==8.1.7 21 | # via uvicorn 22 | fastapi==0.105.0 23 | # via -r requirements.piptools 24 | h11==0.14.0 25 | # via 26 | # httpcore 27 | # uvicorn 28 | httpcore==1.0.2 29 | # via httpx 30 | httpx==0.25.2 31 | # via -r requirements.piptools 32 | idna==3.6 33 | # via 34 | # anyio 35 | # httpx 36 | jinja2==3.1.2 37 | # via -r requirements.piptools 38 | markupsafe==2.1.3 39 | # via jinja2 40 | pydantic==2.5.2 41 | # via fastapi 42 | pydantic-core==2.14.5 43 | # via pydantic 44 | sniffio==1.3.0 45 | # via 46 | # anyio 47 | # httpx 48 | starlette==0.27.0 49 | # via fastapi 50 | typing-extensions==4.9.0 51 | # via 52 | # fastapi 53 | # pydantic 54 | # pydantic-core 55 | uvicorn==0.24.0.post1 56 | # via -r requirements.piptools 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ch06-error-handling-and-perf/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | aiofiles==23.2.1 8 | # via -r requirements.piptools 9 | annotated-types==0.6.0 10 | # via pydantic 11 | anyio==3.7.1 12 | # via 13 | # fastapi 14 | # httpx 15 | # starlette 16 | certifi==2023.11.17 17 | # via 18 | # httpcore 19 | # httpx 20 | click==8.1.7 21 | # via uvicorn 22 | fastapi==0.105.0 23 | # via -r requirements.piptools 24 | h11==0.14.0 25 | # via 26 | # httpcore 27 | # uvicorn 28 | httpcore==1.0.2 29 | # via httpx 30 | httpx==0.25.2 31 | # via -r requirements.piptools 32 | idna==3.6 33 | # via 34 | # anyio 35 | # httpx 36 | jinja2==3.1.2 37 | # via -r requirements.piptools 38 | markupsafe==2.1.3 39 | # via jinja2 40 | pydantic==2.5.2 41 | # via fastapi 42 | pydantic-core==2.14.5 43 | # via pydantic 44 | sniffio==1.3.0 45 | # via 46 | # anyio 47 | # httpx 48 | starlette==0.27.0 49 | # via fastapi 50 | typing-extensions==4.9.0 51 | # via 52 | # fastapi 53 | # pydantic 54 | # pydantic-core 55 | uvicorn==0.24.0.post1 56 | # via -r requirements.piptools 57 | -------------------------------------------------------------------------------- /ch04-language-foundations/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | annotated-types==0.6.0 8 | # via pydantic 9 | anyio==3.7.1 10 | # via httpx 11 | beautifulsoup4==4.12.2 12 | # via bs4 13 | bs4==0.0.1 14 | # via -r requirements.piptools 15 | certifi==2023.11.17 16 | # via 17 | # httpcore 18 | # httpx 19 | # requests 20 | charset-normalizer==3.3.2 21 | # via requests 22 | colorama==0.4.6 23 | # via -r requirements.piptools 24 | h11==0.14.0 25 | # via httpcore 26 | httpcore==1.0.2 27 | # via httpx 28 | httpx==0.25.2 29 | # via -r requirements.piptools 30 | idna==3.6 31 | # via 32 | # anyio 33 | # httpx 34 | # requests 35 | pydantic==2.5.2 36 | # via -r requirements.piptools 37 | pydantic-core==2.14.5 38 | # via pydantic 39 | python-dateutil==2.8.2 40 | # via -r requirements.piptools 41 | requests==2.31.0 42 | # via -r requirements.piptools 43 | six==1.16.0 44 | # via python-dateutil 45 | sniffio==1.3.0 46 | # via 47 | # anyio 48 | # httpx 49 | soupsieve==2.5 50 | # via beautifulsoup4 51 | typing-extensions==4.9.0 52 | # via 53 | # pydantic 54 | # pydantic-core 55 | urllib3==2.1.0 56 | # via requests 57 | -------------------------------------------------------------------------------- /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(file) 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 was updated, and it's type definitions don't match FastAPI, 40 | # but the server and code still work fine. So ignore PyCharm's warning: 41 | # noinspection PyTypeChecker 42 | uvicorn.run(api, port=8000, host='127.0.0.1') 43 | else: 44 | configure() 45 | -------------------------------------------------------------------------------- /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(file) 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 was updated, and it's type definitions don't match FastAPI, 40 | # but the server and code still work fine. So ignore PyCharm's warning: 41 | # noinspection PyTypeChecker 42 | uvicorn.run(api, port=8000, host='127.0.0.1') 43 | else: 44 | configure() 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/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | aiofiles==23.2.1 8 | # via -r requirements.piptools 9 | annotated-types==0.6.0 10 | # via pydantic 11 | anyio==3.7.1 12 | # via 13 | # fastapi 14 | # httpx 15 | # starlette 16 | certifi==2023.11.17 17 | # via 18 | # httpcore 19 | # httpx 20 | # requests 21 | charset-normalizer==3.3.2 22 | # via requests 23 | click==8.1.7 24 | # via uvicorn 25 | fastapi==0.105.0 26 | # via -r requirements.piptools 27 | h11==0.14.0 28 | # via 29 | # httpcore 30 | # uvicorn 31 | httpcore==1.0.2 32 | # via httpx 33 | httpx==0.25.2 34 | # via -r requirements.piptools 35 | idna==3.6 36 | # via 37 | # anyio 38 | # httpx 39 | # requests 40 | jinja2==3.1.2 41 | # via -r requirements.piptools 42 | markupsafe==2.1.3 43 | # via jinja2 44 | pydantic==2.5.2 45 | # via fastapi 46 | pydantic-core==2.14.5 47 | # via pydantic 48 | requests==2.31.0 49 | # via -r requirements.piptools 50 | sniffio==1.3.0 51 | # via 52 | # anyio 53 | # httpx 54 | starlette==0.27.0 55 | # via fastapi 56 | typing-extensions==4.9.0 57 | # via 58 | # fastapi 59 | # pydantic 60 | # pydantic-core 61 | urllib3==2.1.0 62 | # via requests 63 | uvicorn==0.24.0.post1 64 | # via -r requirements.piptools 65 | -------------------------------------------------------------------------------- /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 = {'time': datetime.datetime.now(), 'value': value} 26 | __cache[key] = data 27 | __clean_out_of_date() 28 | 29 | 30 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 31 | if not city or not country or not units: 32 | raise Exception('City, country, and units are required') 33 | 34 | if not state: 35 | state = '' 36 | 37 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 38 | 39 | 40 | def __clean_out_of_date(): 41 | for key, data in list(__cache.items()): 42 | dt = datetime.datetime.now() - data.get('time') 43 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 44 | del __cache[key] 45 | -------------------------------------------------------------------------------- /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 = {'time': datetime.datetime.now(), 'value': value} 26 | __cache[key] = data 27 | __clean_out_of_date() 28 | 29 | 30 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 31 | if not city or not country or not units: 32 | raise Exception('City, country, and units are required') 33 | 34 | if not state: 35 | state = '' 36 | 37 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 38 | 39 | 40 | def __clean_out_of_date(): 41 | for key, data in list(__cache.items()): 42 | dt = datetime.datetime.now() - data.get('time') 43 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 44 | del __cache[key] 45 | -------------------------------------------------------------------------------- /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 = {'time': datetime.datetime.now(), 'value': value} 26 | __cache[key] = data 27 | __clean_out_of_date() 28 | 29 | 30 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]: 31 | if not city or not country or not units: 32 | raise Exception('City, country, and units are required') 33 | 34 | if not state: 35 | state = '' 36 | 37 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower() 38 | 39 | 40 | def __clean_out_of_date(): 41 | for key, data in list(__cache.items()): 42 | dt = datetime.datetime.now() - data.get('time') 43 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours: 44 | del __cache[key] 45 | -------------------------------------------------------------------------------- /ch08-deployment/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | aiofiles==23.2.1 8 | # via -r requirements.piptools 9 | annotated-types==0.6.0 10 | # via pydantic 11 | anyio==3.7.1 12 | # via 13 | # fastapi 14 | # httpx 15 | # starlette 16 | certifi==2023.11.17 17 | # via 18 | # httpcore 19 | # httpx 20 | # requests 21 | charset-normalizer==3.3.2 22 | # via requests 23 | click==8.1.7 24 | # via uvicorn 25 | fastapi==0.105.0 26 | # via -r requirements.piptools 27 | h11==0.14.0 28 | # via 29 | # httpcore 30 | # uvicorn 31 | httpcore==1.0.2 32 | # via httpx 33 | httptools==0.6.1 34 | # via -r requirements.piptools 35 | httpx==0.25.2 36 | # via -r requirements.piptools 37 | idna==3.6 38 | # via 39 | # anyio 40 | # httpx 41 | # requests 42 | jinja2==3.1.2 43 | # via -r requirements.piptools 44 | markupsafe==2.1.3 45 | # via jinja2 46 | pydantic==2.5.2 47 | # via fastapi 48 | pydantic-core==2.14.5 49 | # via pydantic 50 | requests==2.31.0 51 | # via -r requirements.piptools 52 | sniffio==1.3.0 53 | # via 54 | # anyio 55 | # httpx 56 | starlette==0.27.0 57 | # via fastapi 58 | typing-extensions==4.9.0 59 | # via 60 | # fastapi 61 | # pydantic 62 | # pydantic-core 63 | urllib3==2.1.0 64 | # via requests 65 | uvicorn==0.24.0.post1 66 | # via -r requirements.piptools 67 | uvloop==0.19.0 68 | # via -r requirements.piptools 69 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.11 3 | # by the following command: 4 | # 5 | # pip-compile requirements.piptools 6 | # 7 | aiofiles==23.2.1 8 | # via -r requirements.piptools 9 | annotated-types==0.6.0 10 | # via pydantic 11 | anyio==3.7.1 12 | # via 13 | # fastapi 14 | # httpx 15 | # starlette 16 | beautifulsoup4==4.12.2 17 | # via bs4 18 | bs4==0.0.1 19 | # via -r requirements.piptools 20 | certifi==2023.11.17 21 | # via 22 | # httpcore 23 | # httpx 24 | # requests 25 | charset-normalizer==3.3.2 26 | # via requests 27 | click==8.1.7 28 | # via uvicorn 29 | colorama==0.4.6 30 | # via -r requirements.piptools 31 | fastapi==0.105.0 32 | # via -r requirements.piptools 33 | h11==0.14.0 34 | # via 35 | # httpcore 36 | # uvicorn 37 | httpcore==1.0.2 38 | # via httpx 39 | httptools==0.6.1 40 | # via -r requirements.piptools 41 | httpx==0.25.2 42 | # via -r requirements.piptools 43 | idna==3.6 44 | # via 45 | # anyio 46 | # httpx 47 | # requests 48 | jinja2==3.1.2 49 | # via -r requirements.piptools 50 | markupsafe==2.1.3 51 | # via jinja2 52 | pydantic==2.5.2 53 | # via fastapi 54 | pydantic-core==2.14.5 55 | # via pydantic 56 | requests==2.31.0 57 | # via -r requirements.piptools 58 | sniffio==1.3.0 59 | # via 60 | # anyio 61 | # httpx 62 | soupsieve==2.5 63 | # via beautifulsoup4 64 | starlette==0.27.0 65 | # via fastapi 66 | typing-extensions==4.9.0 67 | # via 68 | # fastapi 69 | # pydantic 70 | # pydantic-core 71 | urllib3==2.1.0 72 | # via requests 73 | uvicorn==0.24.0.post1 74 | # via -r requirements.piptools 75 | uvloop==0.19.0 76 | # via -r requirements.piptools 77 | -------------------------------------------------------------------------------- /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 | 9 | async def get_html(episode_number: int) -> str: 10 | print(Fore.YELLOW + f'Getting HTML for episode {episode_number}', flush=True) 11 | 12 | url = f'https://talkpython.fm/{episode_number}' 13 | 14 | async with httpx.AsyncClient() as client: 15 | resp = await client.get(url, follow_redirects=True) 16 | resp.raise_for_status() 17 | 18 | return resp.text 19 | 20 | 21 | def get_title(html: str, episode_number: int) -> str: 22 | print(Fore.CYAN + f'Getting TITLE for episode {episode_number}', flush=True) 23 | soup = bs4.BeautifulSoup(html, 'html.parser') 24 | header = soup.select_one('h1') 25 | if not header: 26 | return 'MISSING' 27 | 28 | return header.text.strip() 29 | 30 | 31 | def main(): 32 | t0 = datetime.datetime.now() 33 | 34 | # Changed this line from the video due to changes in Python 3.10: 35 | # DeprecationWarning: There is no current event loop, loop = asyncio.get_event_loop() 36 | asyncio.run(get_title_range()) 37 | 38 | dt = datetime.datetime.now() - t0 39 | print(f'Done in {dt.total_seconds():.2f} sec.') 40 | 41 | 42 | async def get_title_range_old_version(): 43 | # Please keep this range pretty small to not DDoS my site. ;) 44 | for n in range(270, 280): 45 | html = await get_html(n) 46 | title = get_title(html, n) 47 | print(Fore.WHITE + f'Title found: {title}', flush=True) 48 | 49 | 50 | async def get_title_range(): 51 | # Please keep this range pretty small to not DDoS my site. ;) 52 | 53 | tasks = [] 54 | for n in range(270, 280): 55 | tasks.append((n, asyncio.create_task(get_html(n)))) 56 | 57 | for n, t in tasks: 58 | html = await t 59 | title = get_title(html, n) 60 | print(Fore.WHITE + f'Title found: {title}', flush=True) 61 | 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /ch04-language-foundations/models/orders_v1.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dateutil.parser import parse 4 | 5 | order_json = {'item_id': '123', 'created_date': '2002-11-24 12:22', 'pages_visited': [1, 2, '3'], 'price': 17.22} 6 | 7 | 8 | # class Order: 9 | # 10 | # def __init__(self, item_id: int, created_date: datetime.datetime, 11 | # pages_visited: list[int], price: float): 12 | # self.item_id = item_id 13 | # self.created_date = created_date 14 | # self.pages_visited = pages_visited 15 | # self.price = price 16 | # 17 | # def __str__(self): 18 | # return str(self.__dict__) 19 | 20 | 21 | class Order: 22 | def __init__(self, item_id: int, created_date: datetime.datetime, price: float, pages_visited=None): 23 | if pages_visited is None: 24 | pages_visited = [] 25 | 26 | try: 27 | self.item_id = int(item_id) 28 | except ValueError: 29 | raise Exception('Invalid item_id, it must be an integer.') 30 | 31 | try: 32 | self.created_date = parse(created_date) 33 | except: 34 | raise Exception('Invalid created_date, it must be an datetime.') 35 | 36 | try: 37 | self.price = float(price) 38 | except ValueError: 39 | raise Exception('Invalid price, it must be an float.') 40 | 41 | try: 42 | self.pages_visited = [int(p) for p in pages_visited] 43 | except: 44 | raise Exception('Invalid page list, it must be iterable and contain only integers.') 45 | 46 | def __str__(self): 47 | return ( 48 | f'item_id={self.item_id}, created_date={repr(self.created_date)}, ' 49 | f'price={self.price}, pages_visited={self.pages_visited}' 50 | ) 51 | 52 | def __eq__(self, other): 53 | return isinstance(other, Order) and self.__dict__ == other.__dict__ 54 | 55 | def __ne__(self, other): 56 | return isinstance(other, Order) and self.__dict__ != other.__dict__ 57 | 58 | 59 | o = Order(**order_json) 60 | print(o) 61 | -------------------------------------------------------------------------------- /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(file) 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 was updated, and it's type definitions don't match FastAPI, 54 | # but the server and code still work fine. So ignore PyCharm's warning: 55 | # noinspection PyTypeChecker 56 | uvicorn.run(api, port=8000, host='127.0.0.1') 57 | else: 58 | configure() 59 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /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( 38 | city: str, state: Optional[str], country: Optional[str], units: str 39 | ) -> Tuple[str, Optional[str], str, str]: 40 | city = city.lower().strip() 41 | if not country: 42 | country = 'us' 43 | else: 44 | country = country.lower().strip() 45 | 46 | if len(country) != 2: 47 | error = f'Invalid country: {country}. It must be a two letter abbreviation such as US or GB.' 48 | raise ValidationError(status_code=400, error_msg=error) 49 | 50 | if state: 51 | state = state.strip().lower() 52 | 53 | if state and len(state) != 2: 54 | error = f'Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only).' 55 | raise ValidationError(status_code=400, error_msg=error) 56 | 57 | if units: 58 | units = units.strip().lower() 59 | 60 | valid_units = {'standard', 'metric', 'imperial'} 61 | if units not in valid_units: 62 | error = f"Invalid units '{units}', it must be one of {valid_units}." 63 | raise ValidationError(status_code=400, error_msg=error) 64 | 65 | return city, state, country, units 66 | -------------------------------------------------------------------------------- /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( 38 | city: str, state: Optional[str], country: Optional[str], units: str 39 | ) -> Tuple[str, Optional[str], str, str]: 40 | city = city.lower().strip() 41 | if not country: 42 | country = 'us' 43 | else: 44 | country = country.lower().strip() 45 | 46 | if len(country) != 2: 47 | error = f'Invalid country: {country}. It must be a two letter abbreviation such as US or GB.' 48 | raise ValidationError(status_code=400, error_msg=error) 49 | 50 | if state: 51 | state = state.strip().lower() 52 | 53 | if state and len(state) != 2: 54 | error = f'Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only).' 55 | raise ValidationError(status_code=400, error_msg=error) 56 | 57 | if units: 58 | units = units.strip().lower() 59 | 60 | valid_units = {'standard', 'metric', 'imperial'} 61 | if units not in valid_units: 62 | error = f"Invalid units '{units}', it must be one of {valid_units}." 63 | raise ValidationError(status_code=400, error_msg=error) 64 | 65 | return city, state, country, units 66 | -------------------------------------------------------------------------------- /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( 38 | city: str, state: Optional[str], country: Optional[str], units: str 39 | ) -> Tuple[str, Optional[str], str, str]: 40 | city = city.lower().strip() 41 | if not country: 42 | country = 'us' 43 | else: 44 | country = country.lower().strip() 45 | 46 | if len(country) != 2: 47 | error = f'Invalid country: {country}. It must be a two letter abbreviation such as US or GB.' 48 | raise ValidationError(status_code=400, error_msg=error) 49 | 50 | if state: 51 | state = state.strip().lower() 52 | 53 | if state and len(state) != 2: 54 | error = f'Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only).' 55 | raise ValidationError(status_code=400, error_msg=error) 56 | 57 | if units: 58 | units = units.strip().lower() 59 | 60 | valid_units = {'standard', 'metric', 'imperial'} 61 | if units not in valid_units: 62 | error = f"Invalid units '{units}', it must be one of {valid_units}." 63 | raise ValidationError(status_code=400, error_msg=error) 64 | 65 | return city, state, country, units 66 | -------------------------------------------------------------------------------- /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 %} -------------------------------------------------------------------------------- /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(file) 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 | 45 | # Changed this from the video due to changes in Python 3.10: 46 | # DeprecationWarning: There is no current event loop, loop = asyncio.get_event_loop() 47 | loop = asyncio.new_event_loop() 48 | 49 | try: 50 | loc = Location(city='Portland', state='OR', country='US') 51 | loop.run_until_complete(report_service.add_report('Misty sunrise today, beautiful!', loc)) 52 | loop.run_until_complete(report_service.add_report('Clouds over downtown.', loc)) 53 | except RuntimeError: 54 | print( 55 | 'Note: Could not import starter date, this fails on some systems and ' 56 | 'some ways of running the app under uvicorn.' 57 | ) 58 | print('Fake starter data will no appear on home page.') 59 | print('Once you add data with the client, it will appear properly.') 60 | 61 | 62 | if __name__ == '__main__': 63 | configure() 64 | # uvicorn was updated, and it's type definitions don't match FastAPI, 65 | # but the server and code still work fine. So ignore PyCharm's warning: 66 | # noinspection PyTypeChecker 67 | uvicorn.run(api, port=8000, host='127.0.0.1') 68 | else: 69 | configure() 70 | -------------------------------------------------------------------------------- /.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 | .idea/.gitignore 132 | .idea/fastapi-course.iml 133 | .idea/modules.xml 134 | .idea/vcs.xml 135 | .idea/workspace.xml 136 | .idea/inspectionProfiles/profiles_settings.xml 137 | .idea/inspectionProfiles/Project_Default.xml 138 | /.idea/misc.xml 139 | -------------------------------------------------------------------------------- /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/ohmyzsh/ohmyzsh/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 | 28 | 29 | # Web app file structure 30 | mkdir /apps 31 | chmod 777 /apps 32 | mkdir /apps/logs 33 | mkdir /apps/logs/weather_api 34 | mkdir /apps/logs/weather_api/app_log 35 | # chmod 777 /apps/logs/weather_api 36 | setfacl -m u:apiuser:rwx /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 | # NOTE: These steps have changed since the recording. 75 | 76 | ####### NEW STEPS ############################################### 77 | # See https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal&tab=standard 78 | 79 | # Because always a good idea :) 80 | apt update 81 | apt upgrade 82 | 83 | # Not need even though it's in the instructions, is installed on Ubuntu 84 | # Skip -> install snapd https://snapcraft.io/docs/installing-snapd 85 | 86 | snap install --classic certbot 87 | ln -s /snap/bin/certbot /usr/bin/certbot 88 | certbot --nginx -d weatherapi.talkpython.com 89 | 90 | ####### THESE ARE THE OLD STEPS ################################# 91 | # 92 | ## https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04 93 | # 94 | #add-apt-repository ppa:certbot/certbot 95 | #apt install python-certbot-nginx 96 | #certbot --nginx -d weatherapi.talkpython.com 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern APIs with FastAPI course 2 | 3 | A course from Talk Python Training. Sign up at [**talkpython.fm/fastapi**](https://talkpython.fm/fastapi). 4 | 5 | [![](./readme_resources/fastapi-modern.png)](https://talkpython.fm/fastapi) 6 | 7 | ## Course Summary 8 | 9 | FastAPI is one of the most exciting new web frameworks out today. It's exciting because it leverages more of the modern Python language features than any other framework: type hints, async and await, dataclasses, and much more. If you are building an API in Python, you have many choices. But, to us, FastAPI is the clear choice going forward. And this course will teach you everything you need to know to get started. We'll build a realistic API working with live data and deploy that API to a cloud server Linux VM. In fact, you'll even see how to create proper HTML web pages to augment your API all within FastAPI. 10 | 11 | ## What's this course about and how is it different? 12 | 13 | This course is **designed to get you creating new APIs running in the cloud with FastAPIs quickly**. We start off with just a little foundational concepts, then jump right into build our first API with FastAPI. 14 | 15 | Then we explore the foundational modern Python features to make sure you're ready to take full advantage of this framework. We'll look at how async and await works in Python, how to build self-validating and describing classes with Pydantic, Python 3's type hints, and other core language concepts. 16 | 17 | We round out the course by building a realistic API working with live data. Then we deploy that API using nginx + gunicorn + uvicorn running on Ubuntu in a cloud VM at Digital Ocean. 18 | 19 | ## What topics are covered 20 | 21 | In this course, you will: 22 | 23 | - **See how simple working with basic APIs** in FastAPI can be. 24 | - Create API methods that **handle common HTTP verbs** (GET, POST, DELETE, etc) 25 | - **Return JSON data** to API clients 26 | - **Use async and await** to create truly scalable applications 27 | - **Leverage Pydantic** to create required and optional data exchange 28 | - Have FastAPI **automatically validate and convert data types** (e.g. "2021-01-05" to a `datetime`) 29 | - Organize your app using APIRoutes to **properly factor your application** across Python files. 30 | - Return the **most appropriate error response** (e.g. 400 Bad Request) to API clients 31 | - To deploy Python web applications in production-ready configurations on Linux 32 | - Understand why gunicorn and uvicorn should be used together in production 33 | - And lots more 34 | 35 | View the [full course outline](https://training.talkpython.fm/courses/getting-started-with-fastapi). 36 | 37 | ## Who is this course for? 38 | 39 | This course is for anyone who wants to build an API with Python as the backend language. If you want your API to rival the speed and features of any major web API framework, this is the course to take. 40 | 41 | The **student requirements are quite light for this course**. You'll need Basic Python language knowledge: 42 | 43 | - Functions 44 | - Strings 45 | - Variables 46 | - API clients (making a call with requests) 47 | 48 | Note: All software used during this course, including editors, Python language, etc., are 100% free and open source. **You won't have to buy anything to take the course**. 49 | 50 | ## Sound good? 51 | 52 | If this sounds like a great course for you, take it over at [**talkpython.fm/fastapi**](https://talkpython.fm/fastapi). 53 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------