Skip to content

Commit

Permalink
first sketch ex-11 episodes fetch js-quote
Browse files Browse the repository at this point in the history
  • Loading branch information
steinsiv committed Nov 15, 2023
1 parent 8052cfd commit 8cca9a5
Show file tree
Hide file tree
Showing 16 changed files with 525 additions and 1 deletion.
4 changes: 3 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint",
"ms-azuretools.vscode-docker",
"charliermarsh.ruff"
// "lzm0x219.vscode-markdown-github"
],
"settings": {
Expand All @@ -34,6 +35,7 @@
"trufflehog" : "curl -L https://github.com/trufflesecurity/trufflehog/releases/download/v3.62.1/trufflehog_3.62.1_linux_amd64.tar.gz| tar -xz -C /tmp/ && sudo mv /tmp/trufflehog /usr/local/bin/"
},
"containerEnv": {
"PIPELINE": "development"
"PIPELINE": "development",
"PYTHONDONTWRITEBYTECODE": "1"
}
}
24 changes: 24 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"cSpell.words": [
"authlib",
"azuretools",
"Bealor",
"charliermarsh",
"codespaces",
"dbaeumer",
"devcontainer",
"eitsupi",
"fastapi",
"humao",
"jwks",
"Kingsroad",
"mhutchie",
"pydantic",
"PYTHONDONTWRITEBYTECODE",
"snyk",
"trufflehog",
"uvicorn"
],
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true
}
160 changes: 160 additions & 0 deletions ex-11/got-episodes-api-python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
1 change: 1 addition & 0 deletions ex-11/got-episodes-api-python/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ex-11
28 changes: 28 additions & 0 deletions ex-11/got-episodes-api-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Python version of Episodes API

Notes

FastAPI - https://fastapi.tiangolo.com/#installation
UviCorn - https://www.uvicorn.org/
Tryout Ruff - https://github.com/astral-sh/ruff


```sh
pip install ruff
pip install fastapi
pip install "uvicorn[standard]"
pip install pydantic
pip install requests
pip install authlib
python -m pip install --upgrade pip
```

Start with uvicorn


```sh
uvicorn main:app --reload --log-level debug

or run with
./main.py
```
Empty file.
55 changes: 55 additions & 0 deletions ex-11/got-episodes-api-python/controller/episodes_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# controllers.py
import logging
import requests
from typing import List
from fastapi import HTTPException
from data.models import Episode
from data.got_demo_data import episodes
from core.config import get_settings

logger = logging.getLogger("uvicorn")
config = get_settings()

def get_all_episodes() -> List[Episode]:
return episodes

def get_random_quote(obo_token):

quote_endpoint = f"{ config.quotes_api_url }api/quote"
quote_headers = {"Authorization": f"Bearer {obo_token}"}
logger.warning(f"{quote_endpoint = }")
quote = requests.get(url= quote_endpoint, headers = quote_headers)
logger.info(f"Got a quote: {quote.json()}")
return quote.json()

## Auxiliary methods

def get_episode(episode_id: int) -> Episode:
episode = next((ep for ep in episodes if ep['id'] == episode_id), None)
if episode:
return episode
else:
raise HTTPException(status_code=404, detail="Episode not found")

def add_episode(episode_data: Episode) -> Episode:
new_episode = episode_data.dict()
new_episode['id'] = len(episodes) + 1
episodes.append(new_episode)
return new_episode

def update_episode(episode_id: int, episode_data: Episode) -> Episode:
for index, current_episode in enumerate(episodes):
if current_episode['id'] == episode_id:
updated_episode = episode_data.dict()
updated_episode['id'] = episode_id
episodes[index] = updated_episode
return updated_episode
else:
raise HTTPException(status_code=404, detail="Episode not found")

def delete_episode(episode_id: int) -> None:
global episodes
episodes = [ep for ep in episodes if ep['id'] != episode_id]
if len(episodes) == len(episodes):
raise HTTPException(status_code=404, detail="Episode not found")

49 changes: 49 additions & 0 deletions ex-11/got-episodes-api-python/core/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from authlib.jose import JsonWebToken
from core.config import get_settings

import base64
import json
import logging
import requests

logger = logging.getLogger("uvicorn")
config = get_settings()

jwks = requests.get(config.jwks_uri).json()

claims_options = {
"iss": { "essential": True, "value": str(config.issuer) },
"aud": { "essential": True, "value": config.api_audience }
}

well_known_conf_url = f"https://login.microsoftonline.com/{config.tenant_id}/v2.0/.well-known/openid-configuration"
tokenEndpoint = requests.get(well_known_conf_url).json()["token_endpoint"]

def authVerify(token):
header = json.loads(base64.b64decode(token.split(".")[0]))
logger.info(f"Verify token with header: {header}")
logger.info(f"Claims options: {claims_options = }")
jwt = JsonWebToken(header["alg"])
claims = jwt.decode(token, jwks, claims_options=claims_options)
claims.validate()
logger.warning("Token verified OK")

def get_obo_token(assertion):
logger.info("Trying use the episode-api token as assertion for a new quote-api token using O-B-O");
headers = {"Content-Type" : "application/x-www-form-urlencoded"}

requestForm = {
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"client_id": config.client_id,
"client_secret": config.client_secret,
"assertion": assertion,
"scope": f"api://{config.quotes_api_uri}/Quote.Read",
"requested_token_use": "on_behalf_of",
}
oboToken = requests.post(tokenEndpoint, data=requestForm, headers=headers).json()
logger.warning(f"Got me an OBO token: {oboToken}")
return oboToken["access_token"]




52 changes: 52 additions & 0 deletions ex-11/got-episodes-api-python/core/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
import uvicorn
import logging
from pydantic import HttpUrl, ValidationError
from pydantic_settings import BaseSettings

logger = logging.getLogger("uvicorn")

class AppSettings(BaseSettings):
tenant_id: str
client_id: str
client_secret: str
episodes_api_uri: str
quotes_api_url: HttpUrl
quotes_api_uri: str
issuer: HttpUrl
port: int
host: str
jwks_uri: HttpUrl
api_audience: str

def get_uvicorn_config():
return uvicorn.Config(
app="main:app", # Specify the ASGI application to run
host="0.0.0.0",
port=3100,
log_level="info",
reload=True,
workers=1, # Number of worker processes. Default is the number of CPU cores
access_log=True,
)

def get_settings():
try:
app_settings = AppSettings(
tenant_id = os.environ['TENANT_ID'],
client_id = os.environ['CLIENT_ID'],
client_secret = os.environ['CLIENT_SECRET'],
episodes_api_uri = os.environ['EPISODES_API_URI'],
quotes_api_uri = os.environ['QUOTES_API_URI'],
quotes_api_url = os.environ['QUOTES_API_URL'],
issuer = f"https://sts.windows.net/{os.environ['TENANT_ID']}/",
port = os.environ['PORT'],
host = os.environ['HOST'],
jwks_uri = f"https://login.microsoftonline.com/{os.environ['TENANT_ID']}/discovery/v2.0/keys",
api_audience= f"api://{os.environ['EPISODES_API_URI']}"
)
except ValidationError as exc:
for err in exc.errors():
logger.warning(f"{err['type']}: {', '.join(err['loc'])}")
exit(1)
return app_settings
Loading

0 comments on commit 8cca9a5

Please sign in to comment.