Skip to content

Commit e285590

Browse files
authored
Framework: BuildManifest Migration, Firmware Range Refactor, and Admin INFO Resync Tools (#159)
1 parent 99e3444 commit e285590

File tree

14 files changed

+1222
-322
lines changed

14 files changed

+1222
-322
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""Split Build Firmware and add Build Manifest
2+
3+
Revision ID: 30bc0c0455f8
4+
Revises: f95855ce9471
5+
Create Date: 2025-11-05 20:43:32.593098
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "30bc0c0455f8"
14+
down_revision = "f95855ce9471"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
bind = op.get_bind()
21+
meta = sa.MetaData()
22+
23+
# 1) Add new columns (nullable for backfill), add FKs
24+
op.add_column("build", sa.Column("firmware_min_id", sa.Integer(), nullable=True))
25+
op.add_column("build", sa.Column("firmware_max_id", sa.Integer(), nullable=True))
26+
op.create_foreign_key(None, "build", "firmware", ["firmware_min_id"], ["id"])
27+
op.create_foreign_key(None, "build", "firmware", ["firmware_max_id"], ["id"])
28+
29+
# 2) Backfill firmware_min_id from legacy firmware_id (still present right now)
30+
op.execute("UPDATE build SET firmware_min_id = firmware_id")
31+
32+
# 3) Make firmware_min_id NOT NULL
33+
op.alter_column(
34+
"build", "firmware_min_id", existing_type=sa.Integer(), nullable=False
35+
)
36+
37+
# 4) Drop old FK and legacy column firmware_id
38+
op.drop_constraint(op.f("build_firmware_id_fkey"), "build", type_="foreignkey")
39+
op.drop_column("build", "firmware_id")
40+
41+
# 5) Create buildmanifest table
42+
op.create_table(
43+
"buildmanifest",
44+
sa.Column("id", sa.Integer(), primary_key=True),
45+
sa.Column("build_id", sa.Integer(), nullable=False, unique=True),
46+
sa.Column("dependencies", sa.Unicode(255)),
47+
sa.Column("conf_dependencies", sa.UnicodeText()),
48+
sa.Column("conflicts", sa.Unicode(255)),
49+
sa.Column("conf_conflicts", sa.UnicodeText()),
50+
sa.Column("conf_privilege", sa.UnicodeText()),
51+
sa.Column("conf_resource", sa.UnicodeText()),
52+
sa.ForeignKeyConstraint(["build_id"], ["build.id"]),
53+
)
54+
55+
# 6) Reflect
56+
build = sa.Table("build", meta, autoload_with=bind)
57+
version = sa.Table("version", meta, autoload_with=bind)
58+
buildmanifest = sa.Table("buildmanifest", meta, autoload_with=bind)
59+
60+
# 7) Backfill buildmanifest: one row per build using its version’s fields
61+
sel = sa.select(
62+
build.c.id.label("build_id"),
63+
version.c.dependencies,
64+
version.c.conf_dependencies,
65+
version.c.conflicts,
66+
version.c.conf_conflicts,
67+
version.c.conf_privilege,
68+
version.c.conf_resource,
69+
).select_from(build.join(version, build.c.version_id == version.c.id))
70+
71+
rows = bind.execute(sel).fetchall()
72+
if rows:
73+
bind.execute(
74+
buildmanifest.insert(),
75+
[
76+
{
77+
"build_id": r.build_id,
78+
"dependencies": r.dependencies,
79+
"conf_dependencies": r.conf_dependencies,
80+
"conflicts": r.conflicts,
81+
"conf_conflicts": r.conf_conflicts,
82+
"conf_privilege": r.conf_privilege,
83+
"conf_resource": r.conf_resource,
84+
}
85+
for r in rows
86+
],
87+
)
88+
89+
# 8) Drop moved columns from version
90+
op.drop_column("version", "conf_resource")
91+
op.drop_column("version", "conf_dependencies")
92+
op.drop_column("version", "conf_conflicts")
93+
op.drop_column("version", "dependencies")
94+
op.drop_column("version", "conf_privilege")
95+
op.drop_column("version", "conflicts")
96+
97+
98+
def downgrade():
99+
bind = op.get_bind()
100+
meta = sa.MetaData()
101+
102+
# 1) Recreate columns on version
103+
op.add_column("version", sa.Column("conflicts", sa.Unicode(255)))
104+
op.add_column("version", sa.Column("conf_privilege", sa.UnicodeText()))
105+
op.add_column("version", sa.Column("dependencies", sa.Unicode(255)))
106+
op.add_column("version", sa.Column("conf_conflicts", sa.UnicodeText()))
107+
op.add_column("version", sa.Column("conf_dependencies", sa.UnicodeText()))
108+
op.add_column("version", sa.Column("conf_resource", sa.UnicodeText()))
109+
110+
# Reflect
111+
version = sa.Table("version", meta, autoload_with=bind)
112+
build = sa.Table("build", meta, autoload_with=bind)
113+
buildmanifest = sa.Table("buildmanifest", meta, autoload_with=bind)
114+
115+
# 2) Copy manifest back to version using the first build (lowest build.id) per version
116+
first_build_cte = (
117+
sa.select(
118+
build.c.version_id,
119+
sa.func.min(build.c.id).label("first_build_id"),
120+
)
121+
.group_by(build.c.version_id)
122+
.cte("first_build")
123+
)
124+
125+
sel = sa.select(
126+
version.c.id.label("version_id"),
127+
buildmanifest.c.dependencies,
128+
buildmanifest.c.conf_dependencies,
129+
buildmanifest.c.conflicts,
130+
buildmanifest.c.conf_conflicts,
131+
buildmanifest.c.conf_privilege,
132+
buildmanifest.c.conf_resource,
133+
).select_from(
134+
version.join(first_build_cte, first_build_cte.c.version_id == version.c.id)
135+
.join(build, build.c.id == first_build_cte.c.first_build_id)
136+
.join(buildmanifest, buildmanifest.c.build_id == build.c.id)
137+
)
138+
139+
rows = bind.execute(sel).fetchall()
140+
for r in rows:
141+
bind.execute(
142+
version.update()
143+
.where(version.c.id == r.version_id)
144+
.values(
145+
dependencies=r.dependencies,
146+
conf_dependencies=r.conf_dependencies,
147+
conflicts=r.conflicts,
148+
conf_conflicts=r.conf_conflicts,
149+
conf_privilege=r.conf_privilege,
150+
conf_resource=r.conf_resource,
151+
)
152+
)
153+
154+
# 3) Drop buildmanifest
155+
op.drop_table("buildmanifest")
156+
157+
# 4) Restore legacy firmware_id and backfill from firmware_min_id
158+
op.add_column("build", sa.Column("firmware_id", sa.Integer(), nullable=True))
159+
160+
op.execute("UPDATE build SET firmware_id = firmware_min_id")
161+
162+
op.alter_column("build", "firmware_id", existing_type=sa.Integer(), nullable=False)
163+
op.create_foreign_key(
164+
op.f("build_firmware_id_fkey"), "build", "firmware", ["firmware_id"], ["id"]
165+
)
166+
167+
# 5) Drop FKs on firmware_min_id / firmware_max_id (names may be auto-generated)
168+
bind = op.get_bind()
169+
insp = sa.inspect(bind)
170+
for fk in insp.get_foreign_keys("build"):
171+
cols = tuple(fk.get("constrained_columns") or ())
172+
if "firmware_min_id" in cols or "firmware_max_id" in cols:
173+
op.drop_constraint(fk["name"], "build", type_="foreignkey")
174+
175+
# 6) Drop the new columns
176+
op.drop_column("build", "firmware_max_id")
177+
op.drop_column("build", "firmware_min_id")

spkrepo/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def create_app(config=None, register_blueprints=True, init_admin=True):
3737
# Enable or disable Flask’s subdomain routing per config
3838
app.subdomain_matching = app.config.get("SUBDOMAIN_MATCHING", False)
3939

40+
# Disable strict slashes
41+
app.url_map.strict_slashes = False
42+
4043
if config is not None:
4144
app.config.from_object(config)
4245

spkrepo/cli.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,21 @@ def create_user(username, email, password):
3030
@with_appcontext
3131
def populate_db():
3232
"""Populate the database with some packages."""
33-
from spkrepo.models import Architecture
33+
from spkrepo.models import Architecture, BuildManifest
3434
from spkrepo.tests.common import BuildFactory, PackageFactory, VersionFactory
3535

36+
def attach_manifest(build, *, dependencies=None):
37+
"""Attach a simple manifest to a build with optional dependencies."""
38+
manifest = BuildManifest(
39+
dependencies=dependencies,
40+
conf_dependencies=None,
41+
conflicts=None,
42+
conf_conflicts=None,
43+
conf_privilege=None,
44+
conf_resource=None,
45+
)
46+
build.buildmanifest = manifest
47+
3648
with db.session.no_autoflush:
3749
# nzbget
3850
nzbget_package = PackageFactory(name="nzbget")
@@ -41,7 +53,6 @@ def populate_db():
4153
package=nzbget_package,
4254
upstream_version="12.0",
4355
version=10,
44-
dependencies=None,
4556
report_url=None,
4657
install_wizard=True,
4758
upgrade_wizard=False,
@@ -50,15 +61,16 @@ def populate_db():
5061
package=nzbget_package,
5162
upstream_version="13.0",
5263
version=11,
53-
dependencies=None,
5464
report_url=None,
5565
install_wizard=True,
5666
upgrade_wizard=False,
5767
),
5868
]
5969
nzbget_builds = []
6070
for version in nzbget_versions:
61-
builds = BuildFactory.create_batch(2, version=version, active=True)
71+
builds = BuildFactory.create_batch(
72+
2, version=version, active=True, buildmanifest=False
73+
)
6274
nzbget_builds.extend(builds)
6375

6476
# sickbeard
@@ -68,8 +80,6 @@ def populate_db():
6880
package=sickbeard_package,
6981
upstream_version="20140528",
7082
version=3,
71-
dependencies="git",
72-
service_dependencies=[],
7383
report_url=None,
7484
install_wizard=False,
7585
upgrade_wizard=False,
@@ -79,8 +89,6 @@ def populate_db():
7989
package=sickbeard_package,
8090
upstream_version="20140702",
8191
version=4,
82-
dependencies="git",
83-
service_dependencies=[],
8492
report_url=None,
8593
install_wizard=False,
8694
upgrade_wizard=False,
@@ -94,8 +102,10 @@ def populate_db():
94102
version=version,
95103
architectures=[Architecture.find("noarch")],
96104
active=True,
105+
buildmanifest=False,
97106
)
98107
)
108+
attach_manifest(sickbeard_builds[-1], dependencies="git")
99109

100110
# git
101111
git_package = PackageFactory(name="git")
@@ -104,8 +114,6 @@ def populate_db():
104114
package=git_package,
105115
upstream_version="1.8.4",
106116
version=3,
107-
dependencies=None,
108-
service_dependencies=[],
109117
report_url=None,
110118
install_wizard=False,
111119
upgrade_wizard=False,
@@ -115,8 +123,6 @@ def populate_db():
115123
package=git_package,
116124
upstream_version="2.1.2",
117125
version=4,
118-
dependencies=None,
119-
service_dependencies=[],
120126
report_url=None,
121127
install_wizard=False,
122128
upgrade_wizard=False,
@@ -125,7 +131,9 @@ def populate_db():
125131
]
126132
git_builds = []
127133
for version in git_versions:
128-
builds = BuildFactory.create_batch(3, version=version, active=True)
134+
builds = BuildFactory.create_batch(
135+
3, version=version, active=True, buildmanifest=False
136+
)
129137
git_builds.extend(builds)
130138

131139
# bitlbee
@@ -135,8 +143,6 @@ def populate_db():
135143
package=bitlbee_package,
136144
upstream_version="3.2.2",
137145
version=9,
138-
dependencies=None,
139-
service_dependencies=[],
140146
report_url=None,
141147
install_wizard=False,
142148
upgrade_wizard=False,
@@ -146,8 +152,6 @@ def populate_db():
146152
package=bitlbee_package,
147153
upstream_version="3.2.3",
148154
version=10,
149-
dependencies=None,
150-
service_dependencies=[],
151155
report_url=None,
152156
install_wizard=False,
153157
upgrade_wizard=False,
@@ -157,16 +161,16 @@ def populate_db():
157161
package=bitlbee_package,
158162
upstream_version="3.3.0",
159163
version=11,
160-
dependencies=None,
161-
service_dependencies=[],
162164
install_wizard=False,
163165
upgrade_wizard=False,
164166
startable=True,
165167
),
166168
]
167169
bitlbee_builds = []
168170
for version in bitlbee_versions:
169-
builds = BuildFactory.create_batch(3, version=version, active=True)
171+
builds = BuildFactory.create_batch(
172+
3, version=version, active=True, buildmanifest=False
173+
)
170174
bitlbee_builds.extend(builds)
171175
db.session.commit()
172176

0 commit comments

Comments
 (0)