|
1 | | -import json |
2 | 1 | import logging |
3 | | -import re |
4 | 2 | import typing as t |
| 3 | +from copy import deepcopy |
| 4 | +from functools import wraps |
5 | 5 |
|
6 | | -import sqlalchemy as sa |
7 | | -from pydantic import BaseModel |
8 | 6 | from quart import g |
9 | 7 | from quart import Quart |
10 | 8 | from quart import request |
11 | | -from quart import Request |
12 | 9 | from quart import Response |
| 10 | +from quart.typing import ResponseReturnValue |
| 11 | +from quart_schema import APIKeySecurityScheme |
| 12 | +from quart_schema import HttpSecurityScheme |
13 | 13 | from quart_schema import QuartSchema |
| 14 | +from werkzeug.utils import import_string |
14 | 15 |
|
15 | | -from quart_sqlalchemy import Base |
16 | | -from quart_sqlalchemy import SQLAlchemyConfig |
17 | | -from quart_sqlalchemy.framework import QuartSQLAlchemy |
18 | | -from quart_sqlalchemy.sim.util import ObjectID |
19 | 16 |
|
20 | | - |
21 | | -AUTHORIZATION_PATTERN = re.compile(r"Bearer (?P<token>.+)") |
22 | 17 | logging.basicConfig(level=logging.INFO) |
23 | 18 | logger = logging.getLogger(__name__) |
24 | 19 |
|
25 | 20 |
|
26 | | -class MyBase(Base): |
27 | | - type_annotation_map = {ObjectID: sa.Integer} |
28 | | - |
29 | | - |
30 | | -app = Quart(__name__) |
31 | | -db = QuartSQLAlchemy( |
32 | | - SQLAlchemyConfig.parse_obj( |
33 | | - { |
34 | | - "model_class": MyBase, |
35 | | - "binds": { |
36 | | - "default": { |
37 | | - "engine": {"url": "sqlite:///file:mem.db?mode=memory&cache=shared&uri=true"}, |
38 | | - "session": {"expire_on_commit": False}, |
39 | | - }, |
40 | | - "read-replica": { |
41 | | - "engine": {"url": "sqlite:///file:mem.db?mode=memory&cache=shared&uri=true"}, |
42 | | - "session": {"expire_on_commit": False}, |
43 | | - "read_only": True, |
44 | | - }, |
45 | | - "async": { |
46 | | - "engine": { |
47 | | - "url": "sqlite+aiosqlite:///file:mem.db?mode=memory&cache=shared&uri=true" |
48 | | - }, |
49 | | - "session": {"expire_on_commit": False}, |
50 | | - }, |
51 | | - }, |
52 | | - } |
53 | | - ) |
| 21 | +BLUEPRINTS = ("quart_sqlalchemy.sim.views.api",) |
| 22 | +EXTENSIONS = ( |
| 23 | + "quart_sqlalchemy.sim.db.db", |
| 24 | + "quart_sqlalchemy.sim.app.schema", |
| 25 | + "quart_sqlalchemy.sim.auth.auth", |
54 | 26 | ) |
55 | | -openapi = QuartSchema(app) |
56 | | - |
57 | | - |
58 | | -class RequestAuth(BaseModel): |
59 | | - client: t.Optional[t.Any] = None |
60 | | - user: t.Optional[t.Any] = None |
61 | | - |
62 | | - @property |
63 | | - def has_client(self): |
64 | | - return self.client is not None |
65 | | - |
66 | | - @property |
67 | | - def has_user(self): |
68 | | - return self.user is not None |
69 | | - |
70 | | - @property |
71 | | - def is_anonymous(self): |
72 | | - return all([self.has_client is False, self.has_user is False]) |
73 | | - |
74 | 27 |
|
75 | | -def get_request_client(request: Request): |
76 | | - api_key = request.headers.get("X-Public-API-Key") |
77 | | - if not api_key: |
78 | | - return |
| 28 | +DEFAULT_CONFIG = { |
| 29 | + "QUART_AUTH_SECURITY_SCHEMES": { |
| 30 | + "public-api-key": APIKeySecurityScheme(in_="header", name="X-Public-API-Key"), |
| 31 | + "session-token-bearer": HttpSecurityScheme(scheme="bearer", bearer_format="opaque"), |
| 32 | + }, |
| 33 | + "REGISTER_BLUEPRINTS": ["quart_sqlalchemy.sim.views.api"], |
| 34 | +} |
79 | 35 |
|
80 | | - with g.bind.Session() as session: |
81 | | - try: |
82 | | - magic_client = g.h.MagicClient(session).get_by_public_api_key(api_key) |
83 | | - except ValueError: |
84 | | - return |
85 | | - else: |
86 | | - return magic_client |
87 | 36 |
|
| 37 | +schema = QuartSchema(security_schemes=DEFAULT_CONFIG["QUART_AUTH_SECURITY_SCHEMES"]) |
88 | 38 |
|
89 | | -def get_request_user(request: Request): |
90 | | - auth_header = request.headers.get("Authorization") |
91 | 39 |
|
92 | | - if not auth_header: |
93 | | - return |
94 | | - m = AUTHORIZATION_PATTERN.match(auth_header) |
95 | | - if m is None: |
96 | | - raise RuntimeError("invalid authorization header") |
| 40 | +def wrap_response(func: t.Callable) -> t.Callable: |
| 41 | + @wraps(func) |
| 42 | + async def decorator(result: ResponseReturnValue) -> Response: |
| 43 | + # import pdb |
97 | 44 |
|
98 | | - auth_token = m.group("auth_token") |
| 45 | + # pdb.set_trace() |
| 46 | + return await func(result) |
99 | 47 |
|
100 | | - with g.bind.Session() as session: |
101 | | - try: |
102 | | - auth_user = g.h.AuthUser(session).get_by_session_token(auth_token) |
103 | | - except ValueError: |
104 | | - return |
105 | | - else: |
106 | | - return auth_user |
| 48 | + return decorator |
107 | 49 |
|
108 | 50 |
|
109 | | -@app.before_request |
110 | | -def set_ethereum_network(): |
111 | | - g.request_network = request.headers.get("X-Fortmatic-Network", "GOERLI").upper() |
| 51 | +def create_app( |
| 52 | + override_config: t.Optional[t.Dict[str, t.Any]] = None, |
| 53 | + extensions: t.Sequence[str] = EXTENSIONS, |
| 54 | + blueprints: t.Sequence[str] = BLUEPRINTS, |
| 55 | +): |
| 56 | + override_config = override_config or {} |
112 | 57 |
|
| 58 | + config = deepcopy(DEFAULT_CONFIG) |
| 59 | + config.update(override_config) |
113 | 60 |
|
114 | | -@app.before_request |
115 | | -def set_bind_handlers_for_request(): |
116 | | - from quart_sqlalchemy.sim.handle import Handlers |
| 61 | + app = Quart(__name__) |
| 62 | + app.config.from_mapping(config) |
117 | 63 |
|
118 | | - g.db = db |
| 64 | + for path in extensions: |
| 65 | + extension = import_string(path) |
| 66 | + extension.init_app(app) |
119 | 67 |
|
120 | | - method = request.method |
121 | | - if method in ["GET", "OPTIONS", "TRACE", "HEAD"]: |
122 | | - bind = "read-replica" |
123 | | - else: |
124 | | - bind = "default" |
| 68 | + for path in blueprints: |
| 69 | + bp = import_string(path) |
| 70 | + app.register_blueprint(bp) |
125 | 71 |
|
126 | | - g.bind = db.get_bind(bind) |
127 | | - g.h = Handlers(g.bind) |
| 72 | + @app.before_request |
| 73 | + def set_ethereum_network(): |
| 74 | + g.network = request.headers.get("X-Ethereum-Network", "GOERLI").upper() |
128 | 75 |
|
| 76 | + # app.make_response = wrap_response(app.make_response) |
129 | 77 |
|
130 | | -@app.before_request |
131 | | -def set_request_auth(): |
132 | | - g.auth = RequestAuth( |
133 | | - client=get_request_client(request), |
134 | | - user=get_request_user(request), |
135 | | - ) |
| 78 | + return app |
136 | 79 |
|
137 | 80 |
|
138 | | -@app.after_request |
139 | | -async def add_json_response_envelope(response: Response) -> Response: |
140 | | - if response.mimetype != "application/json": |
141 | | - return response |
142 | | - data = await response.get_json() |
143 | | - payload = dict(status="ok", message="", data=data) |
144 | | - response.set_data(json.dumps(payload)) |
145 | | - return response |
| 81 | +# @app.after_request |
| 82 | +# async def add_json_response_envelope(response: Response) -> Response: |
| 83 | +# if response.mimetype != "application/json": |
| 84 | +# return response |
| 85 | +# data = await response.get_json() |
| 86 | +# payload = dict(status="ok", message="", data=data) |
| 87 | +# response.set_data(json.dumps(payload)) |
| 88 | +# return response |
0 commit comments