Initial commit with the basics

This commit is contained in:
Scott Idem
2020-09-14 12:41:02 -04:00
parent bd7f33fbbf
commit eeedc2ad6f
16 changed files with 823 additions and 36 deletions

0
app/__init__.py Normal file
View File

18
app/config.py.default Normal file
View File

@@ -0,0 +1,18 @@
import secrets
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
from typing import Any, Dict, List, Optional, Union
class Settings(BaseSettings):
APP_NAME: str = "Aether API"
ADMIN_EMAIL: EmailStr = 'example@example.com'
AETHER_DB_SERVER = 'xxx'
AETHER_DB_PORT = '3306' # default = 3306
AETHER_DB_NAME = 'xxx'
AETHER_DB_USERNAME = 'xxx'
AETHER_DB_PASSWORD = 'xxx'
SQLALCHEMY_DATABASE_URI = 'mysql://'+AETHER_DB_USERNAME+':'+AETHER_DB_PASSWORD+'@'+AETHER_DB_SERVER+'/'+AETHER_DB_NAME
settings = Settings()

171
app/db.py Normal file
View File

@@ -0,0 +1,171 @@
from app.config import settings
from sqlalchemy import create_engine, text
from sqlalchemy.exc import IntegrityError, OperationalError
#from app import db
AMS_DB_SERVER = 'linode.oneskyit.com'
AMS_DB_PORT = '3306' # default = 3306
AMS_DB_NAME = 'aether_dev' #onesky_ams_dev
AMS_DB_USERNAME = 'onesky_aether'
AMS_DB_PASSWORD = '$onesky.Aether.2020'
connection_string = 'mysql://'+AMS_DB_USERNAME+':'+AMS_DB_PASSWORD+'@'+AMS_DB_SERVER+'/'+AMS_DB_NAME
engine = create_engine(name_or_url=connection_string, pool_size=10, pool_recycle=120, pool_pre_ping=True, echo=True, echo_pool=True, isolation_level='READ COMMITTED')
# NOTE: The default isolation_level is 'REPEATABLE READ'. This can sometimes not show updated data.
db = engine.connect()
# Insert a new record with values given.
def sql_insert(table_name=None, record=None, sql=None, data=None):
print('** sql_insert() ***')
if table_name and record:
fields = []
values = []
for key, value in record.items():
if key != 'id': # A special exception for the id auto increment field.
fields.append('`'+str(key)+'`')
values.append(':'+str(key))
fields_string = ', '.join(fields)
values_string = ', '.join(values)
sql_insert = text(
"""
INSERT INTO `"""+table_name+"""` ("""+fields_string+""") VALUES ("""+values_string+""");
"""
)
elif table_name:
sql_insert = text(
"""
INSERT INTO `"""+table_name+"""` () VALUES ();
"""
)
elif sql:
sql_insert = text(sql)
else:
print('One or more required fields are missing')
return False
trans = db.begin()
try:
if record:
result_insert = db.execute(sql_insert, record)
else:
result_insert = db.execute(sql_insert)
trans.commit()
except OperationalError as e:
trans.rollback()
print('*** An exception happened: OperationalError ***')
print('* This is likely because a field that does not exist. *')
print(repr(e))
print('***')
print(str(e))
print('^^^ exception ^^^')
return False
except IntegrityError as e:
trans.rollback()
print('*** An exception happened: IntegrityError ***')
print('* This is likely because of a duplicate entry for a primary or unique field. *')
print(repr(e))
print('***')
print(str(e))
print('^^^ exception ^^^')
return True # NOTE: This is returning True even though there was an exception
except Exception as e:
trans.rollback()
print('*** An exception happened: catch all ***')
print(repr(e))
print('***')
print(str(e))
print('^^^ exception ^^^')
return False
else:
record_id = result_insert.lastrowid
if record_id == 0:
#print('******')
#print(dir(result_insert))
#print('******')
#print(vars(result_insert))
#print('******')
return True
else:
return record_id
# NOTE: Select records using custom SQL SELECT statements.
def sql_select(sql=None, data=None, table_name=None, record_id=None, record_id_random=None, field_name=None, field_value=None, as_list=False):
print('*** sql_select() ***')
if record_id and table_name:
sql = text(
"""
SELECT *
FROM `"""+table_name+"""`
WHERE `"""+table_name+"""`.id = :record_id
"""
)
elif record_id_random and table_name:
sql = text(
"""
SELECT *
FROM `"""+table_name+"""`
WHERE `"""+table_name+"""`.id_random = :record_id_random
"""
)
elif field_name and field_value and table_name:
sql = text(
"""
SELECT *
FROM `"""+table_name+"""`
WHERE `"""+table_name+"""`."""+field_name+""" = :field_value
"""
)
elif table_name:
sql = text(
"""
SELECT *
FROM `"""+table_name+"""`
"""
)
elif sql:
print('SQL found')
sql = text(sql)
else:
print('One or more required fields are missing')
return False
try:
#if record_id or record_id_random:
#result = db.execute(sql, record_id=record_id, record_id_random=record_id_random)
#elif field_name and field_value:
#result = db.execute(sql, field_value=field_value)
#elif sql and data:
#result = db.execute(sql, data)
print('Executing SQL...')
result = db.execute(sql, data=data, record_id=record_id, record_id_random=record_id_random, table_name=table_name, field_name=field_name, field_value=field_value)
except Exception as e:
print('*** An exception happened. ***')
print(repr(e))
print('***')
print(str(e))
print('^^^ exception ^^^')
return False
else:
if result.rowcount == 1 and as_list:
print('Single as list')
record = result.fetchall()
return record
elif result.rowcount == 1 and not as_list:
print('Single as single')
record = result.fetchone()
return record
elif result.rowcount > 1:
print('List as list')
records = result.fetchall()
return records
else:
return False

52
app/lib_general.py Normal file
View File

@@ -0,0 +1,52 @@
from datetime import datetime, time, timedelta
from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union
#router = APIRouter()
#import app
async def get_token_header(x_token: str = Header(...)):
if x_token != 'fake-super-secret-token':
raise HTTPException(status_code=400, detail='X-Token header invalid')
async def get_account_header(x_account_id: str = Header(...)):
print('get_account_header(): '+x_account_id)
return x_account_id
#Add the processing time to the response header.
#@app.middleware('http')
#async def add_process_time_header(request: Request, call_next):
#import time
#start_time = time.time()
#response = await call_next(request)
#process_time = time.time() - start_time
#response.headers['X-Process-Time'] = str(process_time)
#return response
#async def get_token_header(x_token: str = Header(...)):
#if x_token != 'fake-super-secret-token':
#raise HTTPException(status_code=400, detail='X-Token header invalid')
#async def get_account_header(x_account_id: str = Header(...)):
#@app.middleware("http")
#async def get_account_header(x_account_id: str = Header(...)):
#return x_account_id
#x_account_id: str = Header(...)
#x_account_id = 'static random ID...'
#response = await call_next(request)
#print(x_account_id)
#return x_account_id
#async def get_account_header(x_account_id: str = Header(...)):
#print('get_account_header(): '+x_account_id+'z9999z')
#return x_account_id+'z9999z'

93
app/main.py Normal file
View File

@@ -0,0 +1,93 @@
import logging
from datetime import datetime, time, timedelta
from enum import Enum
from fastapi import Body, Cookie, Depends, FastAPI, File, Form, Header, HTTPException, Path, Query, Request, status, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.staticfiles import StaticFiles
from functools import lru_cache
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union
from sqlalchemy import create_engine, text
from sqlalchemy.exc import IntegrityError, OperationalError
from . import config
from .lib_general import *
from .routers import items, users, websockets
app = FastAPI()
@lru_cache()
def get_settings():
return config.Settings()
app.mount('/static', StaticFiles(directory='static'), name='static')
app.include_router(
users.router,
prefix='/user',
tags=['Users'],
#dependencies=[Depends(get_token_header)],
#dependencies=[Depends(get_account_header)],
#responses={404: {'description': 'Not found'}},
)
app.include_router(
items.router,
prefix='/item',
tags=['Items'],
#dependencies=[Depends(get_token_header)],
#responses={404: {'description': 'Not found'}},
)
app.include_router(
websockets.router,
#prefix='/item',
tags=['Websockets'],
#dependencies=[Depends(get_token_header)],
#responses={404: {'description': 'Not found'}},
)
# BEGIN: CORS
origins = [
'http://fastapi.localhost',
'http://localhost',
'http://localhost:5000',
'http://fastapi.localhost:5000',
'https://example.org',
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origin_regex='https://.*\.oneskyit\.com',
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
#expose_headers=[],
#max_age=600,
)
# END: CORS
#Add the processing time to the response header.
@app.middleware('http')
async def add_process_time_header(request: Request, call_next):
import time
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers['X-Process-Time'] = str(process_time)
return response
@app.get('/', tags=['Default'])
async def get_root():
print(config.settings.APP_NAME)
return {'hello': 'This is the Aether API using FastAPI.'}

0
app/routers/__init__.py Normal file
View File

43
app/routers/items.py Normal file
View File

@@ -0,0 +1,43 @@
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union
router = APIRouter()
class Image(BaseModel):
url: str
name: str
class Item(BaseModel):
name: str
description: Optional[str] = Field(None, example='A very nice Item')
price: float
tax: Optional[float] = None
is_offer: Optional[bool] = None
#tags: List[str] = [] # not unique tags
tags: Set[str] = set() # unique tags
image: Optional[Image] = None # one image
images: Optional[List[Image]] = None # or as a list of images
@router.get('/')
async def read_items():
return [{'name': 'Item Foo'}, {'name': 'item Bar'}]
@router.get('/{item_id}')
async def read_item(item_id: str):
return {'name': 'Fake Specific Item', 'item_id': item_id}
@router.put(
'/{item_id}',
tags=['Extra Tag'],
responses={403: {'description': 'Operation forbidden'}},
)
async def update_item(item_id: str):
if item_id != 'foo':
raise HTTPException(status_code=403, detail='You can only update the item: foo')
return {'item_id': item_id, 'name': 'The Fighters'}

View File

@@ -0,0 +1,24 @@
from fastapi import APIRouter, HTTPException
router = APIRouter()
@router.get("/")
async def read_items():
return [{"name": "Item Foo"}, {"name": "item Bar"}]
@router.get("/{item_id}")
async def read_item(item_id: str):
return {"name": "Fake Specific Item", "item_id": item_id}
@router.put(
"/{item_id}",
tags=["custom"],
responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
if item_id != "foo":
raise HTTPException(status_code=403, detail="You can only update the item: foo")
return {"item_id": item_id, "name": "The Fighters"}

153
app/routers/users.py Normal file
View File

@@ -0,0 +1,153 @@
from datetime import datetime, time, timedelta
from fastapi import APIRouter, Depends, Header, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from typing import Dict, List, Optional, Set, Union
from ..lib_general import *
from app.config import settings
from app.db import *
#import logging
router = APIRouter()
class UserBase(BaseModel):
id_random: str = None # This should not be None. It is required.
account_id_random: str = None # This should not be None. It is required.
username: str = Field(None, example='New.User', min_length=3, max_length=100)
name: Optional[str] = None
email: EmailStr
email_verified: Optional[bool] = None
enable: Optional[bool] = None
enable_from: Optional[datetime] = None
enable_to: Optional[datetime] = None
super: Optional[bool] = None
manager: Optional[bool] = None
administrator: Optional[bool] = None
verified: Optional[bool] = None
class UserIn(UserBase):
password: str = Field(None, example='My Difficult Password!', min_length=10)
class UserOut(UserBase):
password_set_on: Optional[datetime] = None
password_reset_token: Optional[str] = None
password_reset_expire_on: Optional[datetime] = None
logged_in_on: Optional[datetime] = None
last_activity_on: Optional[datetime] = None
created_on: datetime
update_on: Optional[datetime] = None
class UserInDB(UserBase):
hashed_password: str
password_set_on: Optional[datetime] = None
password_reset_token: Optional[str] = None
password_reset_expire_on: Optional[datetime] = None
logged_in_on: Optional[datetime] = None
last_activity_on: Optional[datetime] = None
@router.post(
"/",
response_model=UserOut,
summary='Create a new user account',
status_code=status.HTTP_201_CREATED
)
async def create_user(user: UserIn, x_account_id: str = Header(...)):
"""
Create a new user account
"""
user = {}
user['account_id_random'] = x_account_id
user['username'] = 'Scott.Idem'
user['name'] = 'Scott Idem'
user['email'] = 'Scott.Idem@oneskyit.com'
return user
#@router.patch('/{id_random}', response_model=UserOut, dependencies=[Depends(get_account_header)])
#async def update_user(id_random: str, user: UserIn, x_account_id: str = Header(...)):
#async def update_user(id_random: str, user: UserIn):
@router.patch(
'/{id_random}',
response_model=UserOut,
summary='Update a user account'
)
async def update_user(id_random: str, user: UserIn, x_account_id: str = Depends(get_account_header)):
"""
Update a user account
"""
user = {}
user['id_random'] = id_random
user['account_id_random'] = x_account_id
user['username'] = 'Scott.Idem'
user['name'] = 'Scott Idem'
user['email'] = 'Scott.Idem@oneskyit.com'
user['created_on'] = datetime.now()
user['super'] = True
return user
@router.delete('/{id_random}', response_model=bool)
async def delete_user(id_random: str, x_account_id: str = Depends(get_account_header)):
"""
Delete a user account
"""
return True
return False
@router.get('/', response_model=List[UserOut])
@router.get('/list_all', response_model=List[UserOut])
async def list_users():
"""
Get a list of users
"""
print(settings.APP_NAME)
users = [{'username': 'test.user.1'}, {'username': 'test.user.2'}, {'username': 'Scott.Idem'}]
print('Getting all users...')
sql = """
SELECT *
FROM `user`
WHERE id=1
"""
records = sql_select(sql=sql, as_list=True)
#records = sql_select(table_name='user')
if records:
print('Got the user list')
return records
else:
print('No user records found')
raise HTTPException(status_code=404)
@router.get('/{username}')
async def get_user_username(username: str, x_account_id: str = Header(...)):
return {'username': username}
#@router.get('/me')
#async def get_user_current():
#user_out: UserOut
#return {'username': 'test.user'}

View File

@@ -0,0 +1,18 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/users/", tags=["users"])
async def read_users():
return [{"username": "Foo"}, {"username": "Bar"}]
@router.get("/users/me", tags=["users"])
async def read_user_me():
return {"username": "fakecurrentuser"}
@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
return {"username": username}

83
app/routers/websockets.py Normal file
View File

@@ -0,0 +1,83 @@
from fastapi import APIRouter, FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from typing import List
router = APIRouter()
html = """
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<h2>Your ID: <span id="ws-id"></span></h2>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var client_id = Date.now()
document.querySelector("#ws-id").textContent = client_id;
var ws = new WebSocket(`ws://localhost:5005/ws/${client_id}`);
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, websocket: WebSocket):
await websocket.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@router.get("/ws_test")
async def websocket_root():
return HTMLResponse(html)
@router.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.send_personal_message(f"You wrote: {data}", websocket)
await manager.broadcast(f"Client #{client_id} says: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Client #{client_id} left the chat")