[0.5.11]
This commit is contained in:
parent
f3c12f516e
commit
f27b68e350
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
__version__ = '0.5.10'
|
__version__ = '0.5.11'
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
return f'HalfAPI version:{__version__}'
|
return f'HalfAPI version:{__version__}'
|
||||||
|
|
|
@ -17,6 +17,7 @@ from starlette.applications import Starlette
|
||||||
from starlette.authentication import UnauthenticatedUser
|
from starlette.authentication import UnauthenticatedUser
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
|
from starlette.responses import Response, PlainTextResponse
|
||||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||||
|
|
||||||
from timing_asgi import TimingMiddleware
|
from timing_asgi import TimingMiddleware
|
||||||
|
@ -34,12 +35,15 @@ from halfapi.lib.responses import (ORJSONResponse, UnauthorizedResponse,
|
||||||
|
|
||||||
from halfapi.lib.routes import gen_starlette_routes, debug_routes
|
from halfapi.lib.routes import gen_starlette_routes, debug_routes
|
||||||
from halfapi.lib.schemas import get_api_routes, get_api_domain_routes, schema_json, get_acls
|
from halfapi.lib.schemas import get_api_routes, get_api_domain_routes, schema_json, get_acls
|
||||||
|
from halfapi.logging import logger, config_logging
|
||||||
|
from halfapi import __version__
|
||||||
|
|
||||||
logger = logging.getLogger('uvicorn.asgi')
|
|
||||||
|
|
||||||
|
|
||||||
class HalfAPI:
|
class HalfAPI:
|
||||||
def __init__(self, config=None):
|
def __init__(self, config=None):
|
||||||
|
config_logging(logging.DEBUG)
|
||||||
|
|
||||||
if config:
|
if config:
|
||||||
SECRET = config.get('SECRET')
|
SECRET = config.get('SECRET')
|
||||||
PRODUCTION = config.get('PRODUCTION')
|
PRODUCTION = config.get('PRODUCTION')
|
||||||
|
@ -54,16 +58,10 @@ class HalfAPI:
|
||||||
routes = [ Route('/', get_api_routes(DOMAINS)) ]
|
routes = [ Route('/', get_api_routes(DOMAINS)) ]
|
||||||
|
|
||||||
|
|
||||||
routes += [
|
|
||||||
Route('/halfapi/schema', schema_json),
|
|
||||||
Route('/halfapi/acls', get_acls),
|
|
||||||
]
|
|
||||||
|
|
||||||
routes += Route('/halfapi/current_user', lambda request, *args, **kwargs:
|
|
||||||
ORJSONResponse({'user':request.user.json})
|
|
||||||
if SECRET and not isinstance(request.user, UnauthenticatedUser)
|
|
||||||
else ORJSONResponse({'user': None})),
|
|
||||||
|
|
||||||
|
for route in self.routes():
|
||||||
|
routes.append(route)
|
||||||
|
|
||||||
if not PRODUCTION:
|
if not PRODUCTION:
|
||||||
for route in debug_routes():
|
for route in debug_routes():
|
||||||
|
@ -100,6 +98,7 @@ class HalfAPI:
|
||||||
)
|
)
|
||||||
|
|
||||||
if SECRET:
|
if SECRET:
|
||||||
|
self.SECRET = SECRET
|
||||||
self.application.add_middleware(
|
self.application.add_middleware(
|
||||||
AuthenticationMiddleware,
|
AuthenticationMiddleware,
|
||||||
backend=JWTAuthenticationBackend(secret_key=SECRET)
|
backend=JWTAuthenticationBackend(secret_key=SECRET)
|
||||||
|
@ -115,4 +114,23 @@ class HalfAPI:
|
||||||
|
|
||||||
logger.info('CONFIG:\n%s', CONFIG)
|
logger.info('CONFIG:\n%s', CONFIG)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self):
|
||||||
|
return __version__
|
||||||
|
|
||||||
|
async def version_async(self, request, *args, **kwargs):
|
||||||
|
return Response(self.version)
|
||||||
|
|
||||||
|
def routes(self):
|
||||||
|
""" Halfapi default routes
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def get_user(request, *args, **kwargs):
|
||||||
|
return ORJSONResponse({'user':request.user})
|
||||||
|
|
||||||
|
yield Route('/halfapi/whoami', get_user)
|
||||||
|
yield Route('/halfapi/schema', schema_json)
|
||||||
|
yield Route('/halfapi/acls', get_acls)
|
||||||
|
yield Route('/halfapi/version', self.version_async)
|
||||||
|
|
||||||
application = HalfAPI().application
|
application = HalfAPI().application
|
||||||
|
|
|
@ -13,6 +13,7 @@ from .cli import cli
|
||||||
from ..conf import config, write_config, DOMAINSDICT
|
from ..conf import config, write_config, DOMAINSDICT
|
||||||
|
|
||||||
from ..lib.schemas import schema_dict_dom
|
from ..lib.schemas import schema_dict_dom
|
||||||
|
from ..lib.routes import api_routes
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('halfapi')
|
logger = logging.getLogger('halfapi')
|
||||||
|
@ -43,11 +44,15 @@ def list_routes(domain, m_dom):
|
||||||
Echoes the list of the **m_dom** active routes
|
Echoes the list of the **m_dom** active routes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
click.echo(f'\nDomain : {domain}')
|
click.echo(f'\nDomain : {domain}\n')
|
||||||
|
routes = api_routes(m_dom)[0]
|
||||||
|
if len(routes):
|
||||||
|
for key, item in routes.items():
|
||||||
|
methods = '|'.join(list(item.keys()))
|
||||||
|
click.echo(f'\t{key} : {methods}')
|
||||||
|
else:
|
||||||
|
click.echo(f'\t**No ROUTES**')
|
||||||
|
|
||||||
for key, item in schema_dict_dom({domain: m_dom}).get('paths', {}).items():
|
|
||||||
methods = '|'.join(list(item.keys()))
|
|
||||||
click.echo(f'{key} : {methods}')
|
|
||||||
|
|
||||||
|
|
||||||
def list_api_routes():
|
def list_api_routes():
|
||||||
|
|
|
@ -42,8 +42,8 @@ from configparser import ConfigParser
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
from .lib.domain import d_domains
|
from .lib.domain import d_domains
|
||||||
|
from .logging import logger
|
||||||
|
|
||||||
logger = logging.getLogger('uvicorn.asgi')
|
|
||||||
|
|
||||||
PROJECT_NAME = environ.get('HALFAPI_PROJECT_NAME') or os.path.basename(os.getcwd())
|
PROJECT_NAME = environ.get('HALFAPI_PROJECT_NAME') or os.path.basename(os.getcwd())
|
||||||
DOMAINSDICT = lambda: {}
|
DOMAINSDICT = lambda: {}
|
||||||
|
|
|
@ -22,15 +22,27 @@ from starlette.authentication import (
|
||||||
from starlette.requests import HTTPConnection
|
from starlette.requests import HTTPConnection
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
logger = logging.getLogger('halfapi')
|
logger = logging.getLogger('uvicorn.error')
|
||||||
|
|
||||||
|
SECRET=None
|
||||||
try:
|
try:
|
||||||
from ..conf import SECRET
|
from ..conf import SECRET
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
logger.error('Could not import SECRET variable from conf module,'\
|
logger.error('Could not import SECRET variable from conf module,'\
|
||||||
' using HALFAPI_SECRET environment variable')
|
' using HALFAPI_SECRET environment variable')
|
||||||
raise Exception('Missing secret') from exc
|
|
||||||
|
|
||||||
|
class Nobody(UnauthenticatedUser):
|
||||||
|
""" Nobody class
|
||||||
|
|
||||||
|
The default class when no token is passed
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
'id' : '',
|
||||||
|
'token': '',
|
||||||
|
'payload': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class JWTUser(BaseUser):
|
class JWTUser(BaseUser):
|
||||||
|
@ -119,7 +131,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||||
PRODUCTION = conn.scope['app'].debug == False
|
PRODUCTION = conn.scope['app'].debug == False
|
||||||
|
|
||||||
if not token and not is_check_call:
|
if not token and not is_check_call:
|
||||||
return AuthCredentials(), UnauthenticatedUser()
|
return AuthCredentials(), Nobody()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if token and not is_fake_user_id:
|
if token and not is_fake_user_id:
|
||||||
|
@ -142,7 +154,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||||
if token:
|
if token:
|
||||||
return AuthCredentials(), CheckUser(payload['user_id'])
|
return AuthCredentials(), CheckUser(payload['user_id'])
|
||||||
else:
|
else:
|
||||||
return AuthCredentials(), UnauthenticatedUser()
|
return AuthCredentials(), Nobody()
|
||||||
|
|
||||||
|
|
||||||
if PRODUCTION and 'debug' in payload.keys() and payload['debug']:
|
if PRODUCTION and 'debug' in payload.keys() and payload['debug']:
|
||||||
|
@ -177,7 +189,7 @@ class JWTWebSocketAuthenticationBackend(AuthenticationBackend):
|
||||||
) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]:
|
) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]:
|
||||||
|
|
||||||
if self.query_param_name not in conn.query_params:
|
if self.query_param_name not in conn.query_params:
|
||||||
return AuthCredentials(), UnauthenticatedUser()
|
return AuthCredentials(), Nobody()
|
||||||
|
|
||||||
token = conn.query_params[self.query_param_name]
|
token = conn.query_params[self.query_param_name]
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ import orjson
|
||||||
# asgi framework
|
# asgi framework
|
||||||
from starlette.responses import PlainTextResponse, Response, JSONResponse
|
from starlette.responses import PlainTextResponse, Response, JSONResponse
|
||||||
|
|
||||||
|
from .jwt_middleware import JWTUser, Nobody
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'HJSONResponse',
|
'HJSONResponse',
|
||||||
|
@ -82,11 +84,16 @@ class ORJSONResponse(JSONResponse):
|
||||||
list_types = {
|
list_types = {
|
||||||
set
|
set
|
||||||
}
|
}
|
||||||
|
jsonable_types = {
|
||||||
|
JWTUser, Nobody
|
||||||
|
}
|
||||||
|
|
||||||
if type(typ) in str_types:
|
if type(typ) in str_types:
|
||||||
return str(typ)
|
return str(typ)
|
||||||
if type(typ) in list_types:
|
if type(typ) in list_types:
|
||||||
return list(typ)
|
return list(typ)
|
||||||
|
if type(typ) in jsonable_types:
|
||||||
|
return typ.json
|
||||||
|
|
||||||
raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse')
|
raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
def config_logging(level=logging.INFO):
|
||||||
|
|
||||||
|
# When run by 'uvicorn ...', a root handler is already
|
||||||
|
# configured and the basicConfig below does nothing.
|
||||||
|
# To get the desired formatting:
|
||||||
|
logging.getLogger().handlers.clear()
|
||||||
|
|
||||||
|
# 'uvicorn --log-config' is broken so we configure in the app.
|
||||||
|
# https://github.com/encode/uvicorn/issues/511
|
||||||
|
logging.basicConfig(
|
||||||
|
# match gunicorn format
|
||||||
|
format='%(asctime)s [%(process)d] [%(levelname)s] %(message)s',
|
||||||
|
datefmt='[%Y-%m-%d %H:%M:%S %z]',
|
||||||
|
level=level)
|
||||||
|
|
||||||
|
# When run by 'gunicorn -k uvicorn.workers.UvicornWorker ...',
|
||||||
|
# These loggers are already configured and propogating.
|
||||||
|
# So we have double logging with a root logger.
|
||||||
|
# (And setting propagate = False hurts the other usage.)
|
||||||
|
logging.getLogger('uvicorn.asgi').handlers.clear()
|
||||||
|
logging.getLogger('uvicorn.access').handlers.clear()
|
||||||
|
logging.getLogger('uvicorn.error').handlers.clear()
|
||||||
|
logging.getLogger('uvicorn.asgi').propagate = True
|
||||||
|
logging.getLogger('uvicorn.access').propagate = True
|
||||||
|
logging.getLogger('uvicorn.error').propagate = True
|
||||||
|
|
||||||
|
config_logging()
|
||||||
|
logger = logging.getLogger('uvicorn.asgi')
|
|
@ -0,0 +1 @@
|
||||||
|
http 127.0.0.1:3000/halfapi/whoami Authorization:$(http 127.0.0.1:3000/authentication/check email=malves password=papa|jq -r '.token')
|
|
@ -5,9 +5,10 @@ from starlette.testclient import TestClient
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
def test_current_user(project_runner, application_debug):
|
def test_whoami(project_runner, application_debug):
|
||||||
|
# @TODO : test with fake login
|
||||||
c = TestClient(application_debug)
|
c = TestClient(application_debug)
|
||||||
r = c.get('/halfapi/current_user')
|
r = c.get('/halfapi/whoami')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
def test_log(application_debug):
|
def test_log(application_debug):
|
||||||
|
|
Loading…
Reference in New Issue