Skip to content

Commit 4003aba

Browse files
committed
Migrate views to SQLAlchemy 2.0 Query API with SoC refactoring
Replace all legacy `session.query()` / `Model.query` calls in views (api_common, api_keys, dashboard, rules, admin) with SQLAlchemy 2.0 `select()` API. Move DB queries out of views into model classmethods and a new `get_org_rule_stats()` utility, following separation of concerns. Add tests for all new classmethods before implementation. New classmethods: ApiKey/MachineApiKey.get_by_key/user_id/all, Flowspec4/6/RTBH.get_all_ordered/count_active, Community.get_all, Action.get_all/get_all_ordered, RuleWhitelistCache.get_by_rule_ids, User.get_all/get_all_ordered/get_by_uuid, Role/Organization classmethods, ASPath.get_all/get_by_prefix, Log.get_recent_paginated.
1 parent de34b87 commit 4003aba

18 files changed

Lines changed: 480 additions & 101 deletions

flowapp/models/api.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from datetime import datetime
2+
from typing import Optional
3+
from sqlalchemy import select
24
from .base import db
35

46

@@ -20,6 +22,14 @@ def is_expired(self):
2022
else:
2123
return self.expires < datetime.now()
2224

25+
@classmethod
26+
def get_by_key(cls, key: str) -> Optional["ApiKey"]:
27+
return db.session.scalars(select(cls).filter_by(key=key)).first()
28+
29+
@classmethod
30+
def get_by_user_id(cls, user_id: int) -> list:
31+
return db.session.scalars(select(cls).filter_by(user_id=user_id)).all()
32+
2333

2434
class MachineApiKey(db.Model):
2535
id = db.Column(db.Integer, primary_key=True)
@@ -38,3 +48,11 @@ def is_expired(self):
3848
return False # Non-expiring key
3949
else:
4050
return self.expires < datetime.now()
51+
52+
@classmethod
53+
def get_by_key(cls, key: str) -> Optional["MachineApiKey"]:
54+
return db.session.scalars(select(cls).filter_by(key=key)).first()
55+
56+
@classmethod
57+
def get_all(cls) -> list:
58+
return db.session.scalars(select(cls)).all()

flowapp/models/community.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import List, Optional
12
from sqlalchemy import event, select
23
from .base import db
34

@@ -24,6 +25,10 @@ def __init__(self, name, comm, larcomm, extcomm, description, as_path, role_id):
2425
self.as_path = as_path
2526
self.role_id = role_id
2627

28+
@classmethod
29+
def get_all(cls) -> List["Community"]:
30+
return db.session.scalars(select(cls)).all()
31+
2732
@classmethod
2833
def get_whitelistable_communities(cls, id_list):
2934
return db.session.scalars(select(cls).filter(cls.id.in_(id_list))).all()
@@ -42,7 +47,13 @@ class ASPath(db.Model):
4247
prefix = db.Column(db.String(120), unique=True)
4348
as_path = db.Column(db.String(250))
4449

45-
# Methods and initializer
50+
@classmethod
51+
def get_all(cls) -> List["ASPath"]:
52+
return db.session.scalars(select(cls)).all()
53+
54+
@classmethod
55+
def get_by_prefix(cls, prefix: str) -> Optional["ASPath"]:
56+
return db.session.scalars(select(cls).filter_by(prefix=prefix)).first()
4657

4758

4859
# Note: seed data is also defined in migrations/versions/001_baseline.py - keep in sync

flowapp/models/log.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime, timedelta
22

3+
from sqlalchemy import select
34
from flowapp.constants import RuleTypes
45
from .base import db
56

@@ -23,6 +24,17 @@ def __init__(self, time, task, user_id, rule_type, rule_id, author):
2324
self.user_id = user_id
2425
self.author = author
2526

27+
@classmethod
28+
def get_recent_paginated(cls, page: int, per_page: int = 20, weeks: int = 1):
29+
since = datetime.now() - timedelta(weeks=weeks)
30+
return db.paginate(
31+
select(cls).filter(cls.time > since).order_by(cls.time.desc()),
32+
page=page,
33+
per_page=per_page,
34+
max_per_page=None,
35+
error_out=False,
36+
)
37+
2638
@classmethod
2739
def delete_old(cls, days: int = 30):
2840
"""Delete logs older than :param days from the database"""

flowapp/models/organization.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from sqlalchemy import event
1+
from typing import List, Optional
2+
from sqlalchemy import event, select
23
from .base import db
34

45

@@ -27,6 +28,14 @@ def get_users(self):
2728
# self.user is the backref from the user_organization relationship
2829
return self.user
2930

31+
@classmethod
32+
def get_all_ordered(cls) -> List["Organization"]:
33+
return db.session.scalars(select(cls).order_by(cls.name)).all()
34+
35+
@classmethod
36+
def get_by_name(cls, name: str) -> Optional["Organization"]:
37+
return db.session.scalars(select(cls).filter_by(name=name)).first()
38+
3039

3140
# Event listeners for Organization
3241
# Note: seed data is also defined in migrations/versions/001_baseline.py - keep in sync

flowapp/models/rules/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from sqlalchemy import event
1+
from typing import List
2+
from sqlalchemy import event, select
23
from ..base import db
34

45

@@ -30,6 +31,14 @@ def __init__(self, name, command, description, role_id=2):
3031
self.description = description
3132
self.role_id = role_id
3233

34+
@classmethod
35+
def get_all_ordered(cls) -> List["Action"]:
36+
return db.session.scalars(select(cls).order_by(cls.name)).all()
37+
38+
@classmethod
39+
def get_all(cls) -> List["Action"]:
40+
return db.session.scalars(select(cls)).all()
41+
3342

3443
# Event listeners for Rstate
3544
# Note: seed data is also defined in migrations/versions/001_baseline.py - keep in sync

flowapp/models/rules/flowspec.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
from datetime import datetime
3+
from typing import List, Optional
34
from flowapp import utils
5+
from sqlalchemy import func, select
46
from ..base import db
57

68

@@ -163,6 +165,17 @@ def json(self, prefered_format="yearfirst"):
163165
"""
164166
return json.dumps(self.to_dict())
165167

168+
@classmethod
169+
def get_all_ordered(cls) -> List["Flowspec4"]:
170+
return db.session.scalars(select(cls).order_by(cls.expires.desc())).all()
171+
172+
@classmethod
173+
def count_active(cls, org_id: Optional[int] = None) -> int:
174+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1)
175+
if org_id is not None:
176+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1, org_id=org_id)
177+
return db.session.scalar(q)
178+
166179

167180
class Flowspec6(db.Model):
168181
id = db.Column(db.Integer, primary_key=True)
@@ -292,3 +305,14 @@ def json(self, prefered_format="yearfirst"):
292305
:returns: json
293306
"""
294307
return json.dumps(self.to_dict())
308+
309+
@classmethod
310+
def get_all_ordered(cls) -> List["Flowspec6"]:
311+
return db.session.scalars(select(cls).order_by(cls.expires.desc())).all()
312+
313+
@classmethod
314+
def count_active(cls, org_id: Optional[int] = None) -> int:
315+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1)
316+
if org_id is not None:
317+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1, org_id=org_id)
318+
return db.session.scalar(q)

flowapp/models/rules/rtbh.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
from datetime import datetime
3+
from typing import List, Optional
34
from flowapp import utils
5+
from sqlalchemy import func, select
46
from ..base import db
57

68

@@ -151,3 +153,14 @@ def __str__(self):
151153

152154
def get_author(self):
153155
return f"{self.user.email} / {self.org}"
156+
157+
@classmethod
158+
def get_all_ordered(cls) -> List["RTBH"]:
159+
return db.session.scalars(select(cls).order_by(cls.expires.desc())).all()
160+
161+
@classmethod
162+
def count_active(cls, org_id: Optional[int] = None) -> int:
163+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1)
164+
if org_id is not None:
165+
q = select(func.count()).select_from(cls).filter_by(rstate_id=1, org_id=org_id)
166+
return db.session.scalar(q)

flowapp/models/rules/whitelist.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ def delete_by_rule_id(cls, rule_id: int):
156156
db.session.commit()
157157
return deleted
158158

159+
@classmethod
160+
def get_by_rule_ids(cls, rule_ids: list, rule_type: "RuleTypes") -> list:
161+
if not rule_ids:
162+
return []
163+
return db.session.scalars(
164+
select(cls).filter(cls.rid.in_(rule_ids), cls.rtype == rule_type.value)
165+
).all()
166+
159167
@classmethod
160168
def count_by_rule(cls, rule_id: int, rule_type: RuleTypes):
161169
"""

flowapp/models/user.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import List, Optional
12
from sqlalchemy import event, select
23
from .base import db, user_role, user_organization
34
from .organization import Organization
@@ -57,6 +58,18 @@ def update(self, form):
5758

5859
db.session.commit()
5960

61+
@classmethod
62+
def get_all(cls) -> List["User"]:
63+
return db.session.scalars(select(cls)).all()
64+
65+
@classmethod
66+
def get_all_ordered(cls) -> List["User"]:
67+
return db.session.scalars(select(cls).order_by(cls.name)).all()
68+
69+
@classmethod
70+
def get_by_uuid(cls, uuid: str) -> Optional["User"]:
71+
return db.session.scalars(select(cls).filter_by(uuid=uuid)).first()
72+
6073

6174
class Role(db.Model):
6275
id = db.Column(db.Integer, primary_key=True)
@@ -70,6 +83,10 @@ def __init__(self, name, description):
7083
def __repr__(self):
7184
return self.name
7285

86+
@classmethod
87+
def get_all_ordered(cls) -> List["Role"]:
88+
return db.session.scalars(select(cls).order_by(cls.name)).all()
89+
7390

7491
# Event listeners for Role
7592
# Note: seed data is also defined in migrations/versions/001_baseline.py - keep in sync

flowapp/models/utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,47 @@ def check_global_rule_limit(rule_type: RuleTypes) -> bool:
6565
return False
6666

6767

68+
def get_org_rule_stats() -> dict:
69+
"""
70+
Return per-org and global active rule counts for the admin organizations view.
71+
"""
72+
orgs = db.session.scalars(
73+
select(Organization).options(db.joinedload(Organization.rtbh))
74+
).unique().all()
75+
76+
rtbh_counts = dict(
77+
db.session.execute(
78+
select(RTBH.org_id, func.count(RTBH.id))
79+
.filter(RTBH.rstate_id == 1)
80+
.group_by(RTBH.org_id)
81+
).all()
82+
)
83+
flowspec4_counts = dict(
84+
db.session.execute(
85+
select(Flowspec4.org_id, func.count(Flowspec4.id))
86+
.filter(Flowspec4.rstate_id == 1)
87+
.group_by(Flowspec4.org_id)
88+
).all()
89+
)
90+
flowspec6_counts = dict(
91+
db.session.execute(
92+
select(Flowspec6.org_id, func.count(Flowspec6.id))
93+
.filter(Flowspec6.rstate_id == 1)
94+
.group_by(Flowspec6.org_id)
95+
).all()
96+
)
97+
98+
return {
99+
"orgs": orgs,
100+
"rtbh_counts": rtbh_counts,
101+
"flowspec4_counts": flowspec4_counts,
102+
"flowspec6_counts": flowspec6_counts,
103+
"rtbh_all_count": RTBH.count_active(),
104+
"flowspec4_all_count": Flowspec4.count_active(),
105+
"flowspec6_all_count": Flowspec6.count_active(),
106+
}
107+
108+
68109
def get_whitelist_model_if_exists(form_data):
69110
"""
70111
Check if the record in database exist

0 commit comments

Comments
 (0)