Skip to content

Commit e866789

Browse files
simulation
1 parent bdabd4b commit e866789

File tree

20 files changed

+3266
-2
lines changed

20 files changed

+3266
-2
lines changed

src/quart_sqlalchemy/model/mixins.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def to_dict(self):
7979
class RecursiveDictMixin:
8080
__abstract__ = True
8181

82-
def model_to_dict(
82+
def to_dict(
8383
self,
8484
obj: t.Optional[t.Any] = None,
8585
max_depth: int = 3,

src/quart_sqlalchemy/model/model.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
from .mixins import ComparableMixin
1515
from .mixins import DynamicArgsMixin
1616
from .mixins import ReprMixin
17+
from .mixins import SimpleDictMixin
1718
from .mixins import TableNameMixin
1819

1920

2021
sa = sqlalchemy
2122

2223

23-
class Base(DynamicArgsMixin, ReprMixin, ComparableMixin, TableNameMixin):
24+
class Base(DynamicArgsMixin, ReprMixin, SimpleDictMixin, ComparableMixin, TableNameMixin):
2425
__abstract__ = True
2526

2627
type_annotation_map = {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import app
2+
from . import model

src/quart_sqlalchemy/sim/app.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import json
2+
import logging
3+
import re
4+
import typing as t
5+
6+
import sqlalchemy as sa
7+
from pydantic import BaseModel
8+
from quart import g
9+
from quart import Quart
10+
from quart import request
11+
from quart import Request
12+
from quart import Response
13+
from quart_schema import QuartSchema
14+
15+
from .. import Base
16+
from .. import SQLAlchemyConfig
17+
from ..framework import QuartSQLAlchemy
18+
from .util import ObjectID
19+
20+
21+
AUTHORIZATION_PATTERN = re.compile(r"Bearer (?P<token>.+)")
22+
logging.basicConfig(level=logging.INFO)
23+
logger = logging.getLogger(__name__)
24+
25+
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+
)
54+
)
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+
75+
def get_request_client(request: Request):
76+
api_key = request.headers.get("X-Public-API-Key")
77+
if not api_key:
78+
return
79+
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+
88+
89+
def get_request_user(request: Request):
90+
auth_header = request.headers.get("Authorization")
91+
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")
97+
98+
auth_token = m.group("auth_token")
99+
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
107+
108+
109+
@app.before_request
110+
def set_ethereum_network():
111+
g.request_network = request.headers.get("X-Fortmatic-Network", "GOERLI").upper()
112+
113+
114+
@app.before_request
115+
def set_bind_handlers_for_request():
116+
from quart_sqlalchemy.sim.handle import Handlers
117+
118+
g.db = db
119+
120+
method = request.method
121+
if method in ["GET", "OPTIONS", "TRACE", "HEAD"]:
122+
bind = "read-replica"
123+
else:
124+
bind = "default"
125+
126+
g.bind = db.get_bind(bind)
127+
g.h = Handlers(g.bind)
128+
129+
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+
)
136+
137+
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
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from __future__ import annotations
2+
3+
import typing as t
4+
5+
import sqlalchemy
6+
import sqlalchemy.event
7+
import sqlalchemy.exc
8+
import sqlalchemy.orm
9+
import sqlalchemy.sql
10+
from sqlalchemy.orm.interfaces import ORMOption
11+
12+
from quart_sqlalchemy.types import ColumnExpr
13+
from quart_sqlalchemy.types import DMLTable
14+
from quart_sqlalchemy.types import EntityT
15+
from quart_sqlalchemy.types import Selectable
16+
17+
18+
sa = sqlalchemy
19+
20+
21+
class StatementBuilder(t.Generic[EntityT]):
22+
model: t.Type[EntityT]
23+
24+
def __init__(self, model: t.Type[EntityT]):
25+
self.model = model
26+
27+
def complex_select(
28+
self,
29+
selectables: t.Sequence[Selectable] = (),
30+
conditions: t.Sequence[ColumnExpr] = (),
31+
group_by: t.Sequence[t.Union[ColumnExpr, str]] = (),
32+
order_by: t.Sequence[t.Union[ColumnExpr, str]] = (),
33+
options: t.Sequence[ORMOption] = (),
34+
execution_options: t.Optional[t.Dict[str, t.Any]] = None,
35+
offset: t.Optional[int] = None,
36+
limit: t.Optional[int] = None,
37+
distinct: bool = False,
38+
for_update: bool = False,
39+
) -> sa.Select:
40+
statement = sa.select(*selectables or self.model).where(*conditions)
41+
42+
if for_update:
43+
statement = statement.with_for_update()
44+
if offset:
45+
statement = statement.offset(offset)
46+
if limit:
47+
statement = statement.limit(limit)
48+
if group_by:
49+
statement = statement.group_by(*group_by)
50+
if order_by:
51+
statement = statement.order_by(*order_by)
52+
53+
for option in options:
54+
for context in option.context:
55+
for strategy in context.strategy:
56+
if "joined" in strategy:
57+
distinct = True
58+
59+
statement = statement.options(option)
60+
61+
if distinct:
62+
statement = statement.distinct()
63+
64+
if execution_options:
65+
statement = statement.execution_options(**execution_options)
66+
67+
return statement
68+
69+
def insert(
70+
self,
71+
target: t.Optional[DMLTable] = None,
72+
values: t.Optional[t.Dict[str, t.Any]] = None,
73+
) -> sa.Insert:
74+
return sa.insert(target or self.model).values(**values or {})
75+
76+
def bulk_insert(
77+
self,
78+
target: t.Optional[DMLTable] = None,
79+
values: t.Sequence[t.Dict[str, t.Any]] = (),
80+
) -> sa.Insert:
81+
return sa.insert(target or self.model).values(*values)
82+
83+
def bulk_update(
84+
self,
85+
target: t.Optional[DMLTable] = None,
86+
conditions: t.Sequence[ColumnExpr] = (),
87+
values: t.Optional[t.Dict[str, t.Any]] = None,
88+
) -> sa.Update:
89+
return sa.update(target or self.model).where(*conditions).values(**values or {})
90+
91+
def bulk_delete(
92+
self,
93+
target: t.Optional[DMLTable] = None,
94+
conditions: t.Sequence[ColumnExpr] = (),
95+
) -> sa.Delete:
96+
return sa.delete(target or self.model).where(*conditions)

0 commit comments

Comments
 (0)