├── 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 |
4 |
5 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ch07-inbound-data/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
4 |
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 |
5 |
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 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ch07-inbound-data/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
10 |
11 |
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 |
10 |
11 |
12 |
13 |
14 |
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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/.idea/ch06-error-handling-and-perf.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/ch07-inbound-data/.idea/ch07-inbound-data.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 | ''
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 |
49 |
50 |
51 | Copyright © Talk Python Training
52 |
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 |
49 |
50 |
51 | Copyright © Talk Python Training
52 |
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 |
49 |
50 |
51 | Copyright © Talk Python Training
52 |
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 |
49 |
50 |
51 | Copyright © Talk Python Training
52 |
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 |
18 | -
19 | Current weather in a city
20 | GET /api/weather/{city}?country={country}&state={state}
23 |
24 |
25 |
Parameters
26 |
27 | - Required:
city={city} - the city you want to get the weather at.
28 |
29 | - Optional:
state={state} - the state of the city (US only, two letter
30 | abbreviations).
31 |
32 | - Optional:
country={country} - country, US if none specific (two letter
33 | abbreviations).
34 |
35 | - Optional:
units={units} - units: {metric, imperial, standard}, defaults to metric.
36 |
37 |
38 |
39 |
Response JSON
40 |
46 |
47 |
48 |
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 |
18 | -
19 | Current weather in a city
20 | GET /api/weather/{city}?country={country}&state={state}
23 |
24 |
25 |
Parameters
26 |
27 | - Required:
city={city} - the city you want to get the weather at.
28 |
29 | - Optional:
state={state} - the state of the city (US only, two letter
30 | abbreviations).
31 |
32 | - Optional:
country={country} - country, US if none specific (two letter
33 | abbreviations).
34 |
35 | - Optional:
units={units} - units: {metric, imperial, standard}, defaults to metric.
36 |
37 |
38 |
39 |
Response JSON
40 |
46 |
47 |
48 |
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 |
18 | -
19 | Current weather in a city
20 | GET /api/weather/{city}?country={country}&state={state}
23 |
24 |
25 |
Parameters
26 |
27 | - Required:
city={city} - the city you want to get the weather at.
28 |
29 | - Optional:
state={state} - the state of the city (US only, two letter
30 | abbreviations).
31 |
32 | - Optional:
country={country} - country, US if none specific (two letter
33 | abbreviations).
34 |
35 | - Optional:
units={units} - units: {metric, imperial, standard}, defaults to metric.
36 |
37 |
38 |
39 |
Response JSON
40 |
46 |
47 |
48 |
49 |
50 |
51 | {% if events %}
52 |
53 |
Recent weather events
54 |
55 | {% for e in events %}
56 | - {{ e.location.city }}, {{ e.location.country }}: {{ e.description }}
57 | {% endfor %}
58 |
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 |
18 | -
19 | Current weather in a city
20 | GET /api/weather/{city}?country={country}&state={state}
23 |
24 |
25 |
Parameters
26 |
27 | - Required:
city={city} - the city you want to get the weather at.
28 |
29 | - Optional:
state={state} - the state of the city (US only, two letter
30 | abbreviations).
31 |
32 | - Optional:
country={country} - country, US if none specific (two letter
33 | abbreviations).
34 |
35 | - Optional:
units={units} - units: {metric, imperial, standard}, defaults to metric.
36 |
37 |
38 |
39 |
Response JSON
40 |
46 |
47 |
48 |
49 |
50 |
51 | {% if events %}
52 |
53 |
Recent weather events
54 |
55 | {% for e in events %}
56 | - {{ e.location.city }}, {{ e.location.country }}: {{ e.description }}
57 | {% endfor %}
58 |
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 | [](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 | }
--------------------------------------------------------------------------------