├── ch08-deployment
├── .idea
│ ├── .name
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── misc.xml
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── modules.xml
│ └── demo ch08-deployment.iml
├── requirements.txt
├── static
│ ├── img
│ │ ├── cloud.png
│ │ └── favicon.ico
│ └── css
│ │ ├── docs.css
│ │ └── theme.css
├── settings_template.json
├── models
│ ├── location.py
│ ├── validation_error.py
│ └── reports.py
├── views
│ └── home.py
├── server
│ ├── nginx
│ │ └── weather.nginx
│ ├── units
│ │ └── weather.service
│ └── scripts
│ │ └── server_setup.sh
├── services
│ ├── report_service.py
│ └── openweather_service.py
├── bin
│ └── reportapp.py
├── api
│ └── weather_api.py
├── infrastructure
│ └── weather_cache.py
├── main.py
└── templates
│ ├── shared
│ └── layout.html
│ └── home
│ └── index.html
├── ch03-first-api
├── requirements.txt
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── misc.xml
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── modules.xml
│ └── ch03-first-api.iml
└── main.py
├── ch04-language-foundations
├── models
│ ├── requirements.txt
│ ├── orders_v2.py
│ └── orders_v1.py
├── async
│ ├── async_scrape
│ │ ├── requirements.txt
│ │ └── program.py
│ └── sync_scrape
│ │ ├── requirements.txt
│ │ └── program.py
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── misc.xml
│ ├── modules.xml
│ └── ch04-language-foundations.iml
├── asgi
│ └── asgi_summary.py
└── types
│ ├── no_types_program.py
│ └── types_program.py
├── requirements.txt
├── ch05-a-realistic-api
├── requirements.txt
├── static
│ ├── img
│ │ ├── cloud.png
│ │ └── favicon.ico
│ └── css
│ │ ├── docs.css
│ │ └── theme.css
├── settings_template.json
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── misc.xml
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── modules.xml
│ └── ch05-a-realistic-api.iml
├── models
│ └── location.py
├── api
│ └── weather_api.py
├── views
│ └── home.py
├── services
│ └── openweather_service.py
├── main.py
└── templates
│ ├── shared
│ └── layout.html
│ └── home
│ └── index.html
├── ch06-error-handling-and-perf
├── requirements.txt
├── static
│ ├── img
│ │ ├── cloud.png
│ │ └── favicon.ico
│ └── css
│ │ ├── docs.css
│ │ └── theme.css
├── settings_template.json
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── misc.xml
│ ├── modules.xml
│ └── ch06-error-handling-and-perf.iml
├── models
│ ├── location.py
│ └── validation_error.py
├── views
│ └── home.py
├── api
│ └── weather_api.py
├── main.py
├── infrastructure
│ └── weather_cache.py
├── templates
│ ├── shared
│ │ └── layout.html
│ └── home
│ │ └── index.html
└── services
│ └── openweather_service.py
├── ch07-inbound-data
├── requirements.txt
├── static
│ ├── img
│ │ ├── cloud.png
│ │ └── favicon.ico
│ └── css
│ │ ├── docs.css
│ │ └── theme.css
├── settings_template.json
├── .idea
│ ├── codeStyles
│ │ └── codeStyleConfig.xml
│ ├── vcs.xml
│ ├── .gitignore
│ ├── misc.xml
│ ├── inspectionProfiles
│ │ └── profiles_settings.xml
│ ├── modules.xml
│ └── ch07-inbound-data.iml
├── models
│ ├── location.py
│ ├── validation_error.py
│ └── reports.py
├── views
│ └── home.py
├── services
│ ├── report_service.py
│ └── openweather_service.py
├── bin
│ └── reportapp.py
├── api
│ └── weather_api.py
├── infrastructure
│ └── weather_cache.py
├── main.py
└── templates
│ ├── shared
│ └── layout.html
│ └── home
│ └── index.html
├── README.md
└── .gitignore
/ch08-deployment/.idea/.name:
--------------------------------------------------------------------------------
1 | demo ch08-deployment
--------------------------------------------------------------------------------
/ch03-first-api/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 |
4 |
--------------------------------------------------------------------------------
/ch04-language-foundations/models/requirements.txt:
--------------------------------------------------------------------------------
1 | python-dateutil
2 | pydantic
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | httpx
4 | jinja2
5 | aiofiles
6 | requests
7 |
--------------------------------------------------------------------------------
/ch04-language-foundations/async/async_scrape/requirements.txt:
--------------------------------------------------------------------------------
1 | bs4
2 | colorama
3 | httpx
4 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | httpx
4 | jinja2
5 | aiofiles
6 |
--------------------------------------------------------------------------------
/ch04-language-foundations/async/sync_scrape/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 | bs4
3 | colorama
4 |
5 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | httpx
4 | jinja2
5 | aiofiles
6 |
--------------------------------------------------------------------------------
/ch08-deployment/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | httpx
4 | jinja2
5 | aiofiles
6 | requests
7 |
--------------------------------------------------------------------------------
/ch07-inbound-data/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | httpx
4 | jinja2
5 | aiofiles
6 | requests
7 |
--------------------------------------------------------------------------------
/ch08-deployment/static/img/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch08-deployment/static/img/cloud.png
--------------------------------------------------------------------------------
/ch07-inbound-data/static/img/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch07-inbound-data/static/img/cloud.png
--------------------------------------------------------------------------------
/ch07-inbound-data/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch07-inbound-data/static/img/favicon.ico
--------------------------------------------------------------------------------
/ch08-deployment/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch08-deployment/static/img/favicon.ico
--------------------------------------------------------------------------------
/ch05-a-realistic-api/static/img/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch05-a-realistic-api/static/img/cloud.png
--------------------------------------------------------------------------------
/ch05-a-realistic-api/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch05-a-realistic-api/static/img/favicon.ico
--------------------------------------------------------------------------------
/ch07-inbound-data/settings_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org",
3 | "action": "copy this to settings.json with real values."
4 | }
--------------------------------------------------------------------------------
/ch08-deployment/settings_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org",
3 | "action": "copy this to settings.json with real values."
4 | }
--------------------------------------------------------------------------------
/ch05-a-realistic-api/settings_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org",
3 | "action": "copy this to settings.json with real values."
4 | }
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/static/img/cloud.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch06-error-handling-and-perf/static/img/cloud.png
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/settings_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "api_key": "YOUR_API_KEY_FROM openweathermap.org",
3 | "action": "copy this to settings.json with real values."
4 | }
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danshorstein/modern-apis-with-fastapi/main/ch06-error-handling-and-perf/static/img/favicon.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Modern APIs with FastAPI course
2 |
3 | Coming November 2020 from Talk Python Training. Sign up to get notified at [talkpython.fm/friends](https://talkpython.fm/friends).
4 |
--------------------------------------------------------------------------------
/ch03-first-api/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/ch03-first-api/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch03-first-api/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch07-inbound-data/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch07-inbound-data/models/location.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Location(BaseModel):
7 | city: str
8 | state: Optional[str] = None
9 | country: str = 'US'
10 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch08-deployment/models/location.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Location(BaseModel):
7 | city: str
8 | state: Optional[str] = None
9 | country: str = 'US'
10 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/models/location.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Location(BaseModel):
7 | city: str
8 | state: Optional[str] = None
9 | country: str = 'US'
10 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/models/location.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | from pydantic import BaseModel
4 |
5 |
6 | class Location(BaseModel):
7 | city: str
8 | state: Optional[str] = None
9 | country: str = 'US'
10 |
--------------------------------------------------------------------------------
/ch07-inbound-data/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ch03-first-api/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ch07-inbound-data/models/reports.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | from typing import Optional
4 |
5 | from pydantic import BaseModel
6 |
7 | from models.location import Location
8 |
9 |
10 | class ReportSubmittal(BaseModel):
11 | description: str
12 | location: Location
13 |
14 |
15 | class Report(ReportSubmittal):
16 | id: str
17 | created_date: Optional[datetime.datetime]
18 |
--------------------------------------------------------------------------------
/ch08-deployment/models/reports.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | from typing import Optional
4 |
5 | from pydantic import BaseModel
6 |
7 | from models.location import Location
8 |
9 |
10 | class ReportSubmittal(BaseModel):
11 | description: str
12 | location: Location
13 |
14 |
15 | class Report(ReportSubmittal):
16 | id: str
17 | created_date: Optional[datetime.datetime]
18 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/api/weather_api.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import fastapi
4 | from fastapi import Depends
5 |
6 | from models.location import Location
7 | from services import openweather_service
8 |
9 | router = fastapi.APIRouter()
10 |
11 |
12 | @router.get('/api/weather/{city}')
13 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'):
14 | report = await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
15 |
16 | return report
17 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/views/home.py:
--------------------------------------------------------------------------------
1 | import fastapi
2 | from starlette.requests import Request
3 | from starlette.templating import Jinja2Templates
4 |
5 | templates = Jinja2Templates('templates')
6 | router = fastapi.APIRouter()
7 |
8 |
9 | @router.get('/')
10 | def index(request: Request):
11 | return templates.TemplateResponse('home/index.html', {'request': request})
12 |
13 |
14 | @router.get('/favicon.ico')
15 | def favicon():
16 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico')
17 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/views/home.py:
--------------------------------------------------------------------------------
1 | import fastapi
2 | from starlette.requests import Request
3 | from starlette.templating import Jinja2Templates
4 |
5 | templates = Jinja2Templates('templates')
6 | router = fastapi.APIRouter()
7 |
8 |
9 | @router.get('/')
10 | def index(request: Request):
11 | return templates.TemplateResponse('home/index.html', {'request': request})
12 |
13 |
14 | @router.get('/favicon.ico')
15 | def favicon():
16 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico')
17 |
--------------------------------------------------------------------------------
/ch04-language-foundations/asgi/asgi_summary.py:
--------------------------------------------------------------------------------
1 | # Nothing to actually run, just explore these things.
2 |
3 | # What is ASGI
4 |
5 | # WSGI
6 | def request(environ, start_response):
7 | r = start_response(environ)
8 | # ...
9 | return r
10 |
11 |
12 | # ASGI
13 | async def app(scope, receive, send):
14 | r = await receive(scope)
15 | # ...
16 | return await send(r, scope)
17 |
18 |
19 | # Resources
20 | # https://github.com/florimondmanca/awesome-asgi
21 |
22 |
23 | # Server
24 | # uvicorn - https://www.uvicorn.org/
25 |
--------------------------------------------------------------------------------
/ch04-language-foundations/.idea/ch04-language-foundations.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/services/openweather_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | import httpx
3 |
4 | api_key: Optional[str] = None
5 |
6 |
7 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict:
8 | if state:
9 | q = f'{city},{state},{country}'
10 | else:
11 | q = f'{city},{country}'
12 |
13 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}'
14 |
15 | async with httpx.AsyncClient() as client:
16 | resp = await client.get(url)
17 | resp.raise_for_status()
18 |
19 | data = resp.json()
20 | forecast = data['main']
21 | return forecast
22 |
--------------------------------------------------------------------------------
/ch04-language-foundations/models/orders_v2.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import List, Optional
3 |
4 | from dateutil.parser import parse
5 | from pydantic import BaseModel
6 |
7 | order_json = {
8 | 'item_id': '123',
9 | 'created_date': '2002-11-24 12:22',
10 | 'pages_visited': [1, 2, '3'],
11 | 'price': 17.22
12 | }
13 |
14 |
15 | class Order(BaseModel):
16 | item_id: int
17 | created_date: Optional[datetime.datetime]
18 | pages_visited: List[int] = []
19 | price: float
20 |
21 |
22 | o = Order(**order_json)
23 | print(o)
24 |
25 |
26 | # Default for JSON post
27 | # Can be done for others with mods.
28 | def order_api(order: Order):
29 | pass
30 |
--------------------------------------------------------------------------------
/ch07-inbound-data/views/home.py:
--------------------------------------------------------------------------------
1 | import fastapi
2 | from starlette.requests import Request
3 | from starlette.templating import Jinja2Templates
4 |
5 | from services import report_service
6 |
7 | templates = Jinja2Templates('templates')
8 | router = fastapi.APIRouter()
9 |
10 |
11 | @router.get('/', include_in_schema=False)
12 | async def index(request: Request):
13 | events = await report_service.get_reports()
14 | data = {'request': request, 'events': events}
15 |
16 | return templates.TemplateResponse('home/index.html', data)
17 |
18 |
19 | @router.get('/favicon.ico', include_in_schema=False)
20 | def favicon():
21 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico')
22 |
--------------------------------------------------------------------------------
/ch08-deployment/views/home.py:
--------------------------------------------------------------------------------
1 | import fastapi
2 | from starlette.requests import Request
3 | from starlette.templating import Jinja2Templates
4 |
5 | from services import report_service
6 |
7 | templates = Jinja2Templates('templates')
8 | router = fastapi.APIRouter()
9 |
10 |
11 | @router.get('/', include_in_schema=False)
12 | async def index(request: Request):
13 | events = await report_service.get_reports()
14 | data = {'request': request, 'events': events}
15 |
16 | return templates.TemplateResponse('home/index.html', data)
17 |
18 |
19 | @router.get('/favicon.ico', include_in_schema=False)
20 | def favicon():
21 | return fastapi.responses.RedirectResponse(url='/static/img/favicon.ico')
22 |
--------------------------------------------------------------------------------
/ch08-deployment/static/css/docs.css:
--------------------------------------------------------------------------------
1 | .request {
2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
3 | font-weight: bold;
4 | border: 1px solid gray;
5 | border-radius: 5px;
6 | padding: 10px;
7 | font-size: 24px;
8 | }
9 |
10 | .get {
11 | color: #2b542c;
12 | background-color: #beffbd;
13 | }
14 |
15 | .post {
16 | color: #ae5900;
17 | background-color: #ffc79d;
18 | }
19 |
20 | .response_formats span {
21 | font-weight: bold;
22 | color: darkred;
23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
24 | }
25 |
26 | pre {
27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
28 | }
29 |
30 | ul li {
31 | font-size: 18px;
32 | margin-bottom: 10px;
33 | }
--------------------------------------------------------------------------------
/ch05-a-realistic-api/static/css/docs.css:
--------------------------------------------------------------------------------
1 | .request {
2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
3 | font-weight: bold;
4 | border: 1px solid gray;
5 | border-radius: 5px;
6 | padding: 10px;
7 | font-size: 24px;
8 | }
9 |
10 | .get {
11 | color: #2b542c;
12 | background-color: #beffbd;
13 | }
14 |
15 | .post {
16 | color: #ae5900;
17 | background-color: #ffc79d;
18 | }
19 |
20 | .response_formats span {
21 | font-weight: bold;
22 | color: darkred;
23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
24 | }
25 |
26 | pre {
27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
28 | }
29 |
30 | ul li {
31 | font-size: 18px;
32 | margin-bottom: 10px;
33 | }
--------------------------------------------------------------------------------
/ch07-inbound-data/static/css/docs.css:
--------------------------------------------------------------------------------
1 | .request {
2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
3 | font-weight: bold;
4 | border: 1px solid gray;
5 | border-radius: 5px;
6 | padding: 10px;
7 | font-size: 24px;
8 | }
9 |
10 | .get {
11 | color: #2b542c;
12 | background-color: #beffbd;
13 | }
14 |
15 | .post {
16 | color: #ae5900;
17 | background-color: #ffc79d;
18 | }
19 |
20 | .response_formats span {
21 | font-weight: bold;
22 | color: darkred;
23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
24 | }
25 |
26 | pre {
27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
28 | }
29 |
30 | ul li {
31 | font-size: 18px;
32 | margin-bottom: 10px;
33 | }
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/static/css/docs.css:
--------------------------------------------------------------------------------
1 | .request {
2 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
3 | font-weight: bold;
4 | border: 1px solid gray;
5 | border-radius: 5px;
6 | padding: 10px;
7 | font-size: 24px;
8 | }
9 |
10 | .get {
11 | color: #2b542c;
12 | background-color: #beffbd;
13 | }
14 |
15 | .post {
16 | color: #ae5900;
17 | background-color: #ffc79d;
18 | }
19 |
20 | .response_formats span {
21 | font-weight: bold;
22 | color: darkred;
23 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
24 | }
25 |
26 | pre {
27 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
28 | }
29 |
30 | ul li {
31 | font-size: 18px;
32 | margin-bottom: 10px;
33 | }
--------------------------------------------------------------------------------
/ch08-deployment/server/nginx/weather.nginx:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name weatherapi.talkpython.com 128.199.4.0;
4 | server_tokens off;
5 | charset utf-8;
6 |
7 | location /static {
8 | gzip on;
9 | gzip_buffers 8 256k;
10 |
11 | alias /apps/app_repo/ch08-deployment/static;
12 | expires 365d;
13 | }
14 | location / {
15 | try_files $uri @yourapplication;
16 | }
17 | location @yourapplication {
18 | gzip on;
19 | gzip_buffers 8 256k;
20 |
21 | proxy_pass http://127.0.0.1:8000;
22 | proxy_set_header Host $host;
23 | proxy_set_header X-Real-IP $remote_addr;
24 | proxy_set_header X-Forwarded-Protocol $scheme;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/api/weather_api.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import fastapi
4 | from fastapi import Depends
5 |
6 | from models.location import Location
7 | from models.validation_error import ValidationError
8 | from services import openweather_service
9 |
10 | router = fastapi.APIRouter()
11 |
12 |
13 | @router.get('/api/weather/{city}')
14 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'):
15 |
16 | try:
17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
18 | except ValidationError as ve:
19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code)
20 | except Exception as x:
21 | return fastapi.Response(content=str(x), status_code=500)
22 |
--------------------------------------------------------------------------------
/ch03-first-api/.idea/ch03-first-api.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
--------------------------------------------------------------------------------
/ch08-deployment/services/report_service.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | from typing import List
4 |
5 | from models.location import Location
6 | from models.reports import Report
7 |
8 | __reports: List[Report] = []
9 |
10 |
11 | async def get_reports() -> List[Report]:
12 |
13 | # Would be an async call here.
14 | return list(__reports)
15 |
16 |
17 | async def add_report(description: str, location: Location) -> Report:
18 | now = datetime.datetime.now()
19 | report = Report(
20 | id=str(uuid.uuid4()),
21 | location=location,
22 | description=description,
23 | created_date=now)
24 |
25 | # Simulate saving to the DB.
26 | # Would be an async call here.
27 | __reports.append(report)
28 |
29 | __reports.sort(key=lambda r: r.created_date, reverse=True)
30 |
31 | return report
32 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/.idea/ch05-a-realistic-api.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ch07-inbound-data/services/report_service.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import uuid
3 | from typing import List
4 |
5 | from models.location import Location
6 | from models.reports import Report
7 |
8 | __reports: List[Report] = []
9 |
10 |
11 | async def get_reports() -> List[Report]:
12 |
13 | # Would be an async call here.
14 | return list(__reports)
15 |
16 |
17 | async def add_report(description: str, location: Location) -> Report:
18 | now = datetime.datetime.now()
19 | report = Report(
20 | id=str(uuid.uuid4()),
21 | location=location,
22 | description=description,
23 | created_date=now)
24 |
25 | # Simulate saving to the DB.
26 | # Would be an async call here.
27 | __reports.append(report)
28 |
29 | __reports.sort(key=lambda r: r.created_date, reverse=True)
30 |
31 | return report
32 |
--------------------------------------------------------------------------------
/ch08-deployment/server/units/weather.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=gunicorn uvicorn service for Weather Service API
3 | After=syslog.target
4 |
5 | [Service]
6 | ExecStart=/apps/venv/bin/gunicorn -b 127.0.0.1:8000 -w 4 -k uvicorn.workers.UvicornWorker main:api --name weather_svc --chdir /apps/app_repo/ch08-deployment --access-logfile /apps/logs/weather_api/access.log --error-logfile /apps/logs/weather_api/errors.log --user apiuser
7 |
8 | # \/ \/ <- Added post recording for better restart perf.
9 | ExecReload=/bin/kill -s HUP $MAINPID
10 | KillMode=mixed
11 | TimeoutStopSec=5
12 | PrivateTmp=true
13 | # /\ /\ <- Added post recording for better restart perf.
14 |
15 | # Requires systemd version 211 or newer
16 | RuntimeDirectory=/apps/app_repo/ch08-deployment
17 | Restart=always
18 | Type=notify
19 | StandardError=syslog
20 | NotifyAccess=all
21 |
22 |
23 | [Install]
24 | WantedBy=multi-user.target
25 |
--------------------------------------------------------------------------------
/ch08-deployment/.idea/demo ch08-deployment.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------
/ch04-language-foundations/types/no_types_program.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | Item = namedtuple("Item", "name, value")
4 |
5 | running_max = None
6 |
7 |
8 | def counter(items):
9 | global running_max
10 | total = 0
11 |
12 | for i in items:
13 | total += i.value
14 |
15 | if not running_max or total > running_max:
16 | running_max = total
17 |
18 | return total
19 |
20 |
21 | def main():
22 | print("Let's create some items")
23 |
24 | dinner_items = [Item('Pizza', 20), Item('Beer', 9), Item('Beer', 9)]
25 | breakfast_items = [Item('Pancakes', 11), Item('Bacon', 4), Item('Coffee', 3), Item('Coffee', 3), Item('Scone', 2)]
26 |
27 | dinner_total = counter(dinner_items)
28 | print(f"Dinner was ${dinner_total:,.02f}")
29 |
30 | breakfast_total = counter(breakfast_items)
31 | print(f"Breakfast was ${breakfast_total:,.02f}")
32 |
33 | print(f"Today your most expensive meal costs ${running_max:.02f}")
34 |
35 |
36 | if __name__ == '__main__':
37 | main()
38 |
--------------------------------------------------------------------------------
/ch03-first-api/main.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 |
3 | import fastapi
4 | import uvicorn
5 |
6 | api = fastapi.FastAPI()
7 |
8 |
9 | @api.get('/')
10 | def index():
11 | body = "" \
12 | "
" \
13 | "Welcome to the API
" \
14 | "" \
17 | "" \
18 | ""
19 |
20 | return fastapi.responses.HTMLResponse(content=body)
21 |
22 |
23 | @api.get('/api/calculate')
24 | def calculate(x: int, y: int, z: Optional[int] = None):
25 | if z == 0:
26 | return fastapi.responses.JSONResponse(
27 | content={"error": "ERROR: Z cannot be zero."},
28 | status_code=400)
29 |
30 | value = x + y
31 |
32 | if z is not None:
33 | value /= z
34 |
35 | return {
36 | 'x': x,
37 | 'y': y,
38 | 'z': z,
39 | 'value': value
40 | }
41 |
42 |
43 | uvicorn.run(api, port=8000, host="127.0.0.1")
44 |
--------------------------------------------------------------------------------
/ch04-language-foundations/types/types_program.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | from typing import Optional, Iterable
3 |
4 | Item = namedtuple("Item", "name, value")
5 |
6 | running_max: Optional[int] = None
7 |
8 |
9 | def counter(items: Iterable[Item]) -> int:
10 | global running_max
11 |
12 | total = 0
13 |
14 | for i in items:
15 | total += i.value
16 |
17 | if not running_max or total > running_max:
18 | running_max = total
19 |
20 | return total
21 |
22 |
23 | def main():
24 | print("Let's create some items")
25 |
26 | dinner_items = [Item('Pizza', 20), Item('Beer', 9), Item('Beer', 9)]
27 | breakfast_items = [Item('Pancakes', 11), Item('Bacon', 4), Item('Coffee', 3), Item('Coffee', 3), Item('Scone', 2)]
28 |
29 | dinner_total = counter(dinner_items)
30 | print(f"Dinner was ${dinner_total:,.02f}")
31 |
32 | breakfast_total = counter(breakfast_items)
33 | print(f"Breakfast was ${breakfast_total:,.02f}")
34 |
35 | print(f"Today your most expensive meal costs ${running_max:.02f}")
36 |
37 |
38 | if __name__ == '__main__':
39 | main()
40 |
--------------------------------------------------------------------------------
/ch05-a-realistic-api/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | import fastapi
5 | import uvicorn
6 | from starlette.staticfiles import StaticFiles
7 |
8 | from api import weather_api
9 | from services import openweather_service
10 | from views import home
11 |
12 | api = fastapi.FastAPI()
13 |
14 |
15 | def configure():
16 | configure_routing()
17 | configure_api_keys()
18 |
19 |
20 | def configure_api_keys():
21 | file = Path('settings.json').absolute()
22 | if not file.exists():
23 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json")
24 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json")
25 |
26 | with open('settings.json') as fin:
27 | settings = json.load(fin)
28 | openweather_service.api_key = settings.get('api_key')
29 |
30 |
31 | def configure_routing():
32 | api.mount('/static', StaticFiles(directory='static'), name='static')
33 | api.include_router(home.router)
34 | api.include_router(weather_api.router)
35 |
36 |
37 | if __name__ == '__main__':
38 | configure()
39 | uvicorn.run(api, port=8000, host='127.0.0.1')
40 | else:
41 | configure()
42 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | import fastapi
5 | import uvicorn
6 | from starlette.staticfiles import StaticFiles
7 |
8 | from api import weather_api
9 | from services import openweather_service
10 | from views import home
11 |
12 | api = fastapi.FastAPI()
13 |
14 |
15 | def configure():
16 | configure_routing()
17 | configure_api_keys()
18 |
19 |
20 | def configure_api_keys():
21 | file = Path('settings.json').absolute()
22 | if not file.exists():
23 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json")
24 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json")
25 |
26 | with open('settings.json') as fin:
27 | settings = json.load(fin)
28 | openweather_service.api_key = settings.get('api_key')
29 |
30 |
31 | def configure_routing():
32 | api.mount('/static', StaticFiles(directory='static'), name='static')
33 | api.include_router(home.router)
34 | api.include_router(weather_api.router)
35 |
36 |
37 | if __name__ == '__main__':
38 | configure()
39 | uvicorn.run(api, port=8000, host='127.0.0.1')
40 | else:
41 | configure()
42 |
--------------------------------------------------------------------------------
/ch07-inbound-data/bin/reportapp.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | def main():
5 | choice = input("[R]eport weather or [s]ee reports? ")
6 | while choice:
7 | if choice.lower().strip() == 'r':
8 | report_event()
9 | elif choice.lower().strip() == 's':
10 | see_events()
11 | else:
12 | print(f"Don't know what to do with {choice}.")
13 |
14 | choice = input("[R]eport weather or [s]ee reports? ")
15 |
16 |
17 | def report_event():
18 | desc = input("What is happening now? ")
19 | city = input("What city? ")
20 |
21 | data = {
22 | "description": desc,
23 | "location": {
24 | "city": city
25 | }
26 | }
27 |
28 | url = "http://127.0.0.1:8000/api/reports"
29 | resp = requests.post(url, json=data)
30 | resp.raise_for_status()
31 |
32 | result = resp.json()
33 | print(f"Reported new event: {result.get('id')}")
34 |
35 |
36 | def see_events():
37 | url = "http://127.0.0.1:8000/api/reports"
38 | resp = requests.get(url)
39 | resp.raise_for_status()
40 |
41 | data = resp.json()
42 | for r in data:
43 | print(f"{r.get('location').get('city')} has {r.get('description')}")
44 |
45 |
46 | if __name__ == '__main__':
47 | main()
48 |
--------------------------------------------------------------------------------
/ch08-deployment/bin/reportapp.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | def main():
5 | choice = input("[R]eport weather or [s]ee reports? ")
6 | while choice:
7 | if choice.lower().strip() == 'r':
8 | report_event()
9 | elif choice.lower().strip() == 's':
10 | see_events()
11 | else:
12 | print(f"Don't know what to do with {choice}.")
13 |
14 | choice = input("[R]eport weather or [s]ee reports? ")
15 |
16 |
17 | def report_event():
18 | desc = input("What is happening now? ")
19 | city = input("What city? ")
20 |
21 | data = {
22 | "description": desc,
23 | "location": {
24 | "city": city
25 | }
26 | }
27 |
28 | url = "http://127.0.0.1:8000/api/reports"
29 | resp = requests.post(url, json=data)
30 | resp.raise_for_status()
31 |
32 | result = resp.json()
33 | print(f"Reported new event: {result.get('id')}")
34 |
35 |
36 | def see_events():
37 | url = "http://127.0.0.1:8000/api/reports"
38 | resp = requests.get(url)
39 | resp.raise_for_status()
40 |
41 | data = resp.json()
42 | for r in data:
43 | print(f"{r.get('location').get('city')} has {r.get('description')}")
44 |
45 |
46 | if __name__ == '__main__':
47 | main()
48 |
--------------------------------------------------------------------------------
/ch04-language-foundations/async/sync_scrape/program.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import requests
4 | import bs4
5 | from colorama import Fore
6 |
7 |
8 | def get_html(episode_number: int) -> str:
9 | print(Fore.YELLOW + f"Getting HTML for episode {episode_number}", flush=True)
10 |
11 | url = f'https://talkpython.fm/{episode_number}'
12 | resp = requests.get(url)
13 | resp.raise_for_status()
14 |
15 | return resp.text
16 |
17 |
18 | def get_title(html: str, episode_number: int) -> str:
19 | print(Fore.CYAN + f"Getting TITLE for episode {episode_number}", flush=True)
20 | soup = bs4.BeautifulSoup(html, 'html.parser')
21 | header = soup.select_one('h1')
22 | if not header:
23 | return "MISSING"
24 |
25 | return header.text.strip()
26 |
27 |
28 | def main():
29 | t0 = datetime.datetime.now()
30 | get_title_range()
31 | dt = datetime.datetime.now() - t0
32 | print(f"Done in {dt.total_seconds():.2f} sec.")
33 |
34 |
35 | def get_title_range():
36 | # Please keep this range pretty small to not DDoS my site. ;)
37 | for n in range(270, 280):
38 | html = get_html(n)
39 | title = get_title(html, n)
40 | print(Fore.WHITE + f"Title found: {title}", flush=True)
41 |
42 |
43 | if __name__ == '__main__':
44 | main()
45 |
--------------------------------------------------------------------------------
/ch08-deployment/api/weather_api.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 |
3 | import fastapi
4 | from fastapi import Depends
5 |
6 | from models.location import Location
7 | from models.reports import Report, ReportSubmittal
8 | from models.validation_error import ValidationError
9 | from services import openweather_service, report_service
10 |
11 | router = fastapi.APIRouter()
12 |
13 |
14 | @router.get('/api/weather/{city}')
15 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'):
16 | try:
17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
18 | except ValidationError as ve:
19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code)
20 | except Exception as x:
21 | return fastapi.Response(content=str(x), status_code=500)
22 |
23 |
24 | @router.get('/api/reports', name='all_reports', response_model=List[Report])
25 | async def reports_get() -> List[Report]:
26 | # await report_service.add_report("A", Location(city="Portland"))
27 | # await report_service.add_report("B", Location(city="NYC"))
28 | return await report_service.get_reports()
29 |
30 |
31 | @router.post('/api/reports', name='add_report', status_code=201, response_model=Report)
32 | async def reports_post(report_submittal: ReportSubmittal) -> Report:
33 | d = report_submittal.description
34 | loc = report_submittal.location
35 |
36 | return await report_service.add_report(d, loc)
37 |
--------------------------------------------------------------------------------
/ch07-inbound-data/api/weather_api.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, List
2 |
3 | import fastapi
4 | from fastapi import Depends
5 |
6 | from models.location import Location
7 | from models.reports import Report, ReportSubmittal
8 | from models.validation_error import ValidationError
9 | from services import openweather_service, report_service
10 |
11 | router = fastapi.APIRouter()
12 |
13 |
14 | @router.get('/api/weather/{city}')
15 | async def weather(loc: Location = Depends(), units: Optional[str] = 'metric'):
16 | try:
17 | return await openweather_service.get_report_async(loc.city, loc.state, loc.country, units)
18 | except ValidationError as ve:
19 | return fastapi.Response(content=ve.error_msg, status_code=ve.status_code)
20 | except Exception as x:
21 | return fastapi.Response(content=str(x), status_code=500)
22 |
23 |
24 | @router.get('/api/reports', name='all_reports', response_model=List[Report])
25 | async def reports_get() -> List[Report]:
26 | # await report_service.add_report("A", Location(city="Portland"))
27 | # await report_service.add_report("B", Location(city="NYC"))
28 | return await report_service.get_reports()
29 |
30 |
31 | @router.post('/api/reports', name='add_report', status_code=201, response_model=Report)
32 | async def reports_post(report_submittal: ReportSubmittal) -> Report:
33 | d = report_submittal.description
34 | loc = report_submittal.location
35 |
36 | return await report_service.add_report(d, loc)
37 |
--------------------------------------------------------------------------------
/ch07-inbound-data/infrastructure/weather_cache.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Optional, Tuple
3 |
4 | __cache = {}
5 | lifetime_in_hours = 1.0
6 |
7 |
8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]:
9 | key = __create_key(city, state, country, units)
10 | data: dict = __cache.get(key)
11 | if not data:
12 | return None
13 |
14 | last = data['time']
15 | dt = datetime.datetime.now() - last
16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours:
17 | return data['value']
18 |
19 | del __cache[key]
20 | return None
21 |
22 |
23 | def set_weather(city: str, state: str, country: str, units: str, value: dict):
24 | key = __create_key(city, state, country, units)
25 | data = {
26 | 'time': datetime.datetime.now(),
27 | 'value': value
28 | }
29 | __cache[key] = data
30 | __clean_out_of_date()
31 |
32 |
33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]:
34 | if not city or not country or not units:
35 | raise Exception("City, country, and units are required")
36 |
37 | if not state:
38 | state = ""
39 |
40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower()
41 |
42 |
43 | def __clean_out_of_date():
44 | for key, data in list(__cache.items()):
45 | dt = datetime.datetime.now() - data.get('time')
46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours:
47 | del __cache[key]
48 |
--------------------------------------------------------------------------------
/ch08-deployment/infrastructure/weather_cache.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Optional, Tuple
3 |
4 | __cache = {}
5 | lifetime_in_hours = 1.0
6 |
7 |
8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]:
9 | key = __create_key(city, state, country, units)
10 | data: dict = __cache.get(key)
11 | if not data:
12 | return None
13 |
14 | last = data['time']
15 | dt = datetime.datetime.now() - last
16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours:
17 | return data['value']
18 |
19 | del __cache[key]
20 | return None
21 |
22 |
23 | def set_weather(city: str, state: str, country: str, units: str, value: dict):
24 | key = __create_key(city, state, country, units)
25 | data = {
26 | 'time': datetime.datetime.now(),
27 | 'value': value
28 | }
29 | __cache[key] = data
30 | __clean_out_of_date()
31 |
32 |
33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]:
34 | if not city or not country or not units:
35 | raise Exception("City, country, and units are required")
36 |
37 | if not state:
38 | state = ""
39 |
40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower()
41 |
42 |
43 | def __clean_out_of_date():
44 | for key, data in list(__cache.items()):
45 | dt = datetime.datetime.now() - data.get('time')
46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours:
47 | del __cache[key]
48 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/infrastructure/weather_cache.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import Optional, Tuple
3 |
4 | __cache = {}
5 | lifetime_in_hours = 1.0
6 |
7 |
8 | def get_weather(city: str, state: Optional[str], country: str, units: str) -> Optional[dict]:
9 | key = __create_key(city, state, country, units)
10 | data: dict = __cache.get(key)
11 | if not data:
12 | return None
13 |
14 | last = data['time']
15 | dt = datetime.datetime.now() - last
16 | if dt / datetime.timedelta(minutes=60) < lifetime_in_hours:
17 | return data['value']
18 |
19 | del __cache[key]
20 | return None
21 |
22 |
23 | def set_weather(city: str, state: str, country: str, units: str, value: dict):
24 | key = __create_key(city, state, country, units)
25 | data = {
26 | 'time': datetime.datetime.now(),
27 | 'value': value
28 | }
29 | __cache[key] = data
30 | __clean_out_of_date()
31 |
32 |
33 | def __create_key(city: str, state: str, country: str, units: str) -> Tuple[str, str, str, str]:
34 | if not city or not country or not units:
35 | raise Exception("City, country, and units are required")
36 |
37 | if not state:
38 | state = ""
39 |
40 | return city.strip().lower(), state.strip().lower(), country.strip().lower(), units.strip().lower()
41 |
42 |
43 | def __clean_out_of_date():
44 | for key, data in list(__cache.items()):
45 | dt = datetime.datetime.now() - data.get('time')
46 | if dt / datetime.timedelta(minutes=60) > lifetime_in_hours:
47 | del __cache[key]
48 |
--------------------------------------------------------------------------------
/ch07-inbound-data/main.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 | from pathlib import Path
4 |
5 | import fastapi
6 | import uvicorn
7 | from starlette.staticfiles import StaticFiles
8 |
9 | from api import weather_api
10 | from models.location import Location
11 | from services import openweather_service, report_service
12 | from views import home
13 |
14 | api = fastapi.FastAPI()
15 |
16 |
17 | def configure():
18 | configure_routing()
19 | configure_api_keys()
20 | configure_fake_data()
21 |
22 |
23 | def configure_api_keys():
24 | file = Path('settings.json').absolute()
25 | if not file.exists():
26 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json")
27 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json")
28 |
29 | with open('settings.json') as fin:
30 | settings = json.load(fin)
31 | openweather_service.api_key = settings.get('api_key')
32 |
33 |
34 | def configure_routing():
35 | api.mount('/static', StaticFiles(directory='static'), name='static')
36 | api.include_router(home.router)
37 | api.include_router(weather_api.router)
38 |
39 |
40 | def configure_fake_data():
41 | # This was added to make it easier to test the weather event reporting
42 | # We have /api/reports but until you submit new data each run, it's missing
43 | # So this will give us something to start from.
44 | loc = Location(city="Portland", state="OR", country="US")
45 | asyncio.run(report_service.add_report("Misty sunrise today, beautiful!", loc))
46 | asyncio.run(report_service.add_report("Clouds over downtown.", loc))
47 |
48 |
49 | if __name__ == '__main__':
50 | configure()
51 | uvicorn.run(api, port=8000, host='127.0.0.1')
52 | else:
53 | configure()
54 |
--------------------------------------------------------------------------------
/ch08-deployment/main.py:
--------------------------------------------------------------------------------
1 | import json
2 | from pathlib import Path
3 |
4 | import fastapi
5 | import uvicorn
6 | from starlette.staticfiles import StaticFiles
7 |
8 | from api import weather_api
9 | from services import openweather_service
10 | from views import home
11 |
12 | api = fastapi.FastAPI()
13 |
14 |
15 | def configure():
16 | configure_routing()
17 | configure_api_keys()
18 | configure_fake_data()
19 |
20 |
21 | def configure_api_keys():
22 | file = Path('settings.json').absolute()
23 | if not file.exists():
24 | print(f"WARNING: {file} file not found, you cannot continue, please see settings_template.json")
25 | raise Exception("settings.json file not found, you cannot continue, please see settings_template.json")
26 |
27 | with open('settings.json') as fin:
28 | settings = json.load(fin)
29 | openweather_service.api_key = settings.get('api_key')
30 |
31 |
32 | def configure_routing():
33 | api.mount('/static', StaticFiles(directory='static'), name='static')
34 | api.include_router(home.router)
35 | api.include_router(weather_api.router)
36 |
37 |
38 | def configure_fake_data():
39 | # This was added to make it easier to test the weather event reporting
40 | # We have /api/reports but until you submit new data each run, it's missing
41 | # So this will give us something to start from.
42 | pass # Doesn't work on Ubuntu under gunicorn
43 | # try:
44 | # loc = Location(city="Portland", state="OR", country="US")
45 | # asyncio.run(report_service.add_report("Misty sunrise today, beautiful!", loc))
46 | # asyncio.run(report_service.add_report("Clouds over downtown.", loc))
47 | # except:
48 | # print("NOTICE: Add default data not supported on this system (usually under uvicorn on linux)")
49 |
50 |
51 | if __name__ == '__main__':
52 | configure()
53 | uvicorn.run(api, port=8000, host='127.0.0.1')
54 | else:
55 | configure()
56 |
--------------------------------------------------------------------------------
/ch04-language-foundations/async/async_scrape/program.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import datetime
3 |
4 | import httpx
5 | import bs4
6 | from colorama import Fore
7 |
8 | # Older versions of python require calling loop.create_task() rather than on asyncio.
9 | # Make this available more easily.
10 | global loop
11 |
12 |
13 | async def get_html(episode_number: int) -> str:
14 | print(Fore.YELLOW + f"Getting HTML for episode {episode_number}", flush=True)
15 |
16 | url = f'https://talkpython.fm/{episode_number}'
17 |
18 | async with httpx.AsyncClient() as client:
19 | resp = await client.get(url)
20 | resp.raise_for_status()
21 |
22 | return resp.text
23 |
24 |
25 | def get_title(html: str, episode_number: int) -> str:
26 | print(Fore.CYAN + f"Getting TITLE for episode {episode_number}", flush=True)
27 | soup = bs4.BeautifulSoup(html, 'html.parser')
28 | header = soup.select_one('h1')
29 | if not header:
30 | return "MISSING"
31 |
32 | return header.text.strip()
33 |
34 |
35 | def main():
36 | t0 = datetime.datetime.now()
37 |
38 | global loop
39 | loop = asyncio.get_event_loop()
40 | loop.run_until_complete(get_title_range())
41 |
42 | dt = datetime.datetime.now() - t0
43 | print(f"Done in {dt.total_seconds():.2f} sec.")
44 |
45 |
46 | async def get_title_range_old_version():
47 | # Please keep this range pretty small to not DDoS my site. ;)
48 | for n in range(270, 280):
49 | html = await get_html(n)
50 | title = get_title(html, n)
51 | print(Fore.WHITE + f"Title found: {title}", flush=True)
52 |
53 |
54 | async def get_title_range():
55 | # Please keep this range pretty small to not DDoS my site. ;)
56 |
57 | tasks = []
58 | for n in range(270, 280):
59 | tasks.append((n, loop.create_task(get_html(n))))
60 |
61 | for n, t in tasks:
62 | html = await t
63 | title = get_title(html, n)
64 | print(Fore.WHITE + f"Title found: {title}", flush=True)
65 |
66 |
67 | if __name__ == '__main__':
68 | main()
69 |
--------------------------------------------------------------------------------
/ch04-language-foundations/models/orders_v1.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | from typing import List
3 |
4 | from dateutil.parser import parse
5 |
6 | order_json = {
7 | 'item_id': '123',
8 | 'created_date': '2002-11-24 12:22',
9 | 'pages_visited': [1, 2, '3'],
10 | 'price': 17.22
11 | }
12 |
13 |
14 | # class Order:
15 | #
16 | # def __init__(self, item_id: int, created_date: datetime.datetime,
17 | # pages_visited: List[int], price: float):
18 | # self.item_id = item_id
19 | # self.created_date = created_date
20 | # self.pages_visited = pages_visited
21 | # self.price = price
22 | #
23 | # def __str__(self):
24 | # return str(self.__dict__)
25 |
26 | class Order:
27 |
28 | def __init__(self, item_id: int, created_date: datetime.datetime, price: float, pages_visited=None):
29 | if pages_visited is None:
30 | pages_visited = []
31 |
32 | try:
33 | self.item_id = int(item_id)
34 | except ValueError:
35 | raise Exception("Invalid item_id, it must be an integer.")
36 |
37 | try:
38 | self.created_date = parse(created_date)
39 | except:
40 | raise Exception("Invalid created_date, it must be an datetime.")
41 |
42 | try:
43 | self.price = float(price)
44 | except ValueError:
45 | raise Exception("Invalid price, it must be an float.")
46 |
47 | try:
48 | self.pages_visited = [int(p) for p in pages_visited]
49 | except:
50 | raise Exception("Invalid page list, it must be iterable and contain only integers.")
51 |
52 | def __str__(self):
53 | return f'item_id={self.item_id}, created_date={repr(self.created_date)}, ' \
54 | f'price={self.price}, pages_visited={self.pages_visited}'
55 |
56 | def __eq__(self, other):
57 | return isinstance(other, Order) and self.__dict__ == other.__dict__
58 |
59 | def __ne__(self, other):
60 | return isinstance(other, Order) and self.__dict__ == other.__dict__
61 |
62 |
63 | o = Order(**order_json)
64 | print(o)
65 |
--------------------------------------------------------------------------------
/ch07-inbound-data/templates/shared/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Weather API
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {% block content %}
29 |
30 | THIS PAGE HAS NO CONTENT
31 |
32 | {% endblock %}
33 |
34 |
35 |
36 |
37 |
49 |
50 |
51 | Copyright © Talk Python Training
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/ch08-deployment/templates/shared/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Weather API
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {% block content %}
29 |
30 | THIS PAGE HAS NO CONTENT
31 |
32 | {% endblock %}
33 |
34 |
35 |
36 |
37 |
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 |
--------------------------------------------------------------------------------
/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 %}
--------------------------------------------------------------------------------
/ch08-deployment/server/scripts/server_setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Consider running these two commands separately
4 | # Do a reboot before continuing.
5 | apt update
6 | apt upgrade -y
7 |
8 | apt install zsh
9 | sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
10 |
11 | # Install some OS dependencies:
12 | sudo apt-get install -y -q build-essential git unzip zip nload tree
13 | sudo apt-get install -y -q python3-pip python3-dev python3-venv
14 |
15 | # Stop the hackers
16 | sudo apt install fail2ban -y
17 |
18 | ufw allow 22
19 | ufw allow 80
20 | ufw allow 443
21 | ufw enable
22 |
23 |
24 | apt install acl -y
25 | useradd -M apiuser
26 | usermod -L apiuser
27 | setfacl -m u:apiuser:rwx /apps/logs/weather_api
28 |
29 |
30 | # Web app file structure
31 | mkdir /apps
32 | chmod 777 /apps
33 | mkdir /apps/logs
34 | mkdir /apps/logs/weather_api
35 | mkdir /apps/logs/weather_api/app_log
36 | # chmod 777 /apps/logs/weather_api
37 | cd /apps
38 |
39 | # Create a virtual env for the app.
40 | cd /apps
41 | python3 -m venv venv
42 | source /apps/venv/bin/activate
43 | pip install --upgrade pip setuptools wheel
44 | pip install --upgrade httpie glances
45 | pip install --upgrade gunicorn uvloop httptools
46 |
47 | # clone the repo:
48 | cd /apps
49 | git clone https://github.com/talkpython/modern-apis-with-fastapi app_repo
50 |
51 | # Setup the web app:
52 | cd /apps/app_repo/ch08-deployment
53 | pip install -r requirements.txt
54 |
55 | # Copy and enable the daemon
56 | cp /apps/app_repo/ch08-deployment/server/units/weather.service /etc/systemd/system/
57 |
58 | systemctl start weather
59 | systemctl status weather
60 | systemctl enable weather
61 |
62 | # Setup the public facing server (NGINX)
63 | apt install nginx
64 |
65 | # CAREFUL HERE. If you are using default, maybe skip this
66 | rm /etc/nginx/sites-enabled/default
67 |
68 | cp /apps/app_repo/ch08-deployment/server/nginx/weather.nginx /etc/nginx/sites-enabled/
69 | update-rc.d nginx enable
70 | service nginx restart
71 |
72 |
73 | # Optionally add SSL support via Let's Encrypt:
74 | # https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-18-04
75 |
76 | add-apt-repository ppa:certbot/certbot
77 | apt install python3-certbot-nginx
78 | certbot --nginx -d weatherapi.talkpython.com
79 |
--------------------------------------------------------------------------------
/ch07-inbound-data/services/openweather_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Tuple
2 | import httpx
3 | from httpx import Response
4 |
5 | from infrastructure import weather_cache
6 | from models.validation_error import ValidationError
7 |
8 | api_key: Optional[str] = None
9 |
10 |
11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict:
12 | city, state, country, units = validate_units(city, state, country, units)
13 |
14 | if forecast := weather_cache.get_weather(city, state, country, units):
15 | return forecast
16 |
17 | if state:
18 | q = f'{city},{state},{country}'
19 | else:
20 | q = f'{city},{country}'
21 |
22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}'
23 |
24 | async with httpx.AsyncClient() as client:
25 | resp: Response = await client.get(url)
26 | if resp.status_code != 200:
27 | raise ValidationError(resp.text, status_code=resp.status_code)
28 |
29 | data = resp.json()
30 | forecast = data['main']
31 |
32 | weather_cache.set_weather(city, state, country, units, forecast)
33 |
34 | return forecast
35 |
36 |
37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \
38 | Tuple[str, Optional[str], str, str]:
39 | city = city.lower().strip()
40 | if not country:
41 | country = "us"
42 | else:
43 | country = country.lower().strip()
44 |
45 | if len(country) != 2:
46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB."
47 | raise ValidationError(status_code=400, error_msg=error)
48 |
49 | if state:
50 | state = state.strip().lower()
51 |
52 | if state and len(state) != 2:
53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)."
54 | raise ValidationError(status_code=400, error_msg=error)
55 |
56 | if units:
57 | units = units.strip().lower()
58 |
59 | valid_units = {'standard', 'metric', 'imperial'}
60 | if units not in valid_units:
61 | error = f"Invalid units '{units}', it must be one of {valid_units}."
62 | raise ValidationError(status_code=400, error_msg=error)
63 |
64 | return city, state, country, units
65 |
--------------------------------------------------------------------------------
/ch08-deployment/services/openweather_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Tuple
2 | import httpx
3 | from httpx import Response
4 |
5 | from infrastructure import weather_cache
6 | from models.validation_error import ValidationError
7 |
8 | api_key: Optional[str] = None
9 |
10 |
11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict:
12 | city, state, country, units = validate_units(city, state, country, units)
13 |
14 | if forecast := weather_cache.get_weather(city, state, country, units):
15 | return forecast
16 |
17 | if state:
18 | q = f'{city},{state},{country}'
19 | else:
20 | q = f'{city},{country}'
21 |
22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}'
23 |
24 | async with httpx.AsyncClient() as client:
25 | resp: Response = await client.get(url)
26 | if resp.status_code != 200:
27 | raise ValidationError(resp.text, status_code=resp.status_code)
28 |
29 | data = resp.json()
30 | forecast = data['main']
31 |
32 | weather_cache.set_weather(city, state, country, units, forecast)
33 |
34 | return forecast
35 |
36 |
37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \
38 | Tuple[str, Optional[str], str, str]:
39 | city = city.lower().strip()
40 | if not country:
41 | country = "us"
42 | else:
43 | country = country.lower().strip()
44 |
45 | if len(country) != 2:
46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB."
47 | raise ValidationError(status_code=400, error_msg=error)
48 |
49 | if state:
50 | state = state.strip().lower()
51 |
52 | if state and len(state) != 2:
53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)."
54 | raise ValidationError(status_code=400, error_msg=error)
55 |
56 | if units:
57 | units = units.strip().lower()
58 |
59 | valid_units = {'standard', 'metric', 'imperial'}
60 | if units not in valid_units:
61 | error = f"Invalid units '{units}', it must be one of {valid_units}."
62 | raise ValidationError(status_code=400, error_msg=error)
63 |
64 | return city, state, country, units
65 |
--------------------------------------------------------------------------------
/ch06-error-handling-and-perf/services/openweather_service.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Tuple
2 | import httpx
3 | from httpx import Response
4 |
5 | from infrastructure import weather_cache
6 | from models.validation_error import ValidationError
7 |
8 | api_key: Optional[str] = None
9 |
10 |
11 | async def get_report_async(city: str, state: Optional[str], country: str, units: str) -> dict:
12 | city, state, country, units = validate_units(city, state, country, units)
13 |
14 | if forecast := weather_cache.get_weather(city, state, country, units):
15 | return forecast
16 |
17 | if state:
18 | q = f'{city},{state},{country}'
19 | else:
20 | q = f'{city},{country}'
21 |
22 | url = f'https://api.openweathermap.org/data/2.5/weather?q={q}&appid={api_key}&units={units}'
23 |
24 | async with httpx.AsyncClient() as client:
25 | resp: Response = await client.get(url)
26 | if resp.status_code != 200:
27 | raise ValidationError(resp.text, status_code=resp.status_code)
28 |
29 | data = resp.json()
30 | forecast = data['main']
31 |
32 | weather_cache.set_weather(city, state, country, units, forecast)
33 |
34 | return forecast
35 |
36 |
37 | def validate_units(city: str, state: Optional[str], country: Optional[str], units: str) -> \
38 | Tuple[str, Optional[str], str, str]:
39 | city = city.lower().strip()
40 | if not country:
41 | country = "us"
42 | else:
43 | country = country.lower().strip()
44 |
45 | if len(country) != 2:
46 | error = f"Invalid country: {country}. It must be a two letter abbreviation such as US or GB."
47 | raise ValidationError(status_code=400, error_msg=error)
48 |
49 | if state:
50 | state = state.strip().lower()
51 |
52 | if state and len(state) != 2:
53 | error = f"Invalid state: {state}. It must be a two letter abbreviation such as CA or KS (use for US only)."
54 | raise ValidationError(status_code=400, error_msg=error)
55 |
56 | if units:
57 | units = units.strip().lower()
58 |
59 | valid_units = {'standard', 'metric', 'imperial'}
60 | if units not in valid_units:
61 | error = f"Invalid units '{units}', it must be one of {valid_units}."
62 | raise ValidationError(status_code=400, error_msg=error)
63 |
64 | return city, state, country, units
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 | settings.json
131 |
--------------------------------------------------------------------------------
/ch07-inbound-data/templates/home/index.html:
--------------------------------------------------------------------------------
1 | {% extends "shared/layout.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 | Weather Service A RESTful weather service
8 |
9 |
10 |
11 |
12 |
13 |
14 | The Talk Python weather service.
15 |
16 | Endpoints
17 |
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 %}
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------