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

155
.gitignore vendored Normal file → Executable file
View File

@@ -1,33 +1,108 @@
# These are some examples of commonly ignored file patterns. # Byte-compiled / optimized / DLL files
# You should customize this list as applicable to your project. __pycache__/
# Learn more about .gitignore:
# https://www.atlassian.com/git/tutorials/saving-changes/gitignore
# Node artifact files
node_modules/
dist/
# Compiled Java class files
*.class
# Compiled Python bytecode
*.py[cod] *.py[cod]
*$py.class
# Log files # C extensions
*.log *.so
# Package files # Distribution / packaging
*.jar .Python
build/
# Maven develop-eggs/
target/
dist/ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# JetBrains IDE # PyInstaller
.idea/ # 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
# Unit test reports # Installer logs
TEST*.xml pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
environment/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Generated by MacOS # Generated by MacOS
.DS_Store .DS_Store
@@ -35,15 +110,23 @@ TEST*.xml
# Generated by Windows # Generated by Windows
Thumbs.db Thumbs.db
# Applications # Added by Scott Idem
*.app # https://github.com/github/gitignore
*.exe *.sock
*.war *.csv
*.xlsx
# Large media files #*.pdf
*.mp4 *.cfg
*.tiff *.ini
*.avi *.bak
*.flv *.pid
*.mov flask_config.py
*.wmv config.py
#config.cfg
#users.cfg
.directory
tmp/
temp/
development/
myapp/files/
myapp/file_distribution/

View File

@@ -0,0 +1,44 @@
# Go to root of application
cd ~/path/to/directory/my_application/
# Create new application environment
virtualenv environment
# Activate application environment
source environment/bin/activate
# Install application requirements
pip install -r admin/requirements.txt
# Start application
uvicorn app.main:app --host 0.0.0.0 --port 5005 --reload
# View app
http://localhost:5005
# Deactivate environment when done
deactivate
# Use git
# Go to root of application
cd ~/path/to/directory/my_application/
git init
git remote add origin https://scott_idem@bitbucket.org/oneskyit/one-sky-it-api.git
git add .
git commit -m 'Initial commit'
git push -u origin master
git push -u origin development
git push -u origin new-branch-name
# List branches
git branch -a
# Create new branch
git branch new-branch-name
# Switch branch
git switch new-branch-name

4
admin/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
uvicorn
fastapi[all]
SQLAlchemy
mysqlclient

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")

BIN
static/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

1
static/test.txt Normal file
View File

@@ -0,0 +1 @@