From c272772b486f13f00647ac19a384ac49e3ee3eb8 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 18:59:55 +0000 Subject: [PATCH 1/8] core: migration: derive timezone choices from zoneinfo - replaces the vendored pytz.common_timezones list (a TODO from the 2.0 upgrade) with a callable over zoneinfo.available_timezones() - callable choices are serialized by reference, so tzdata updates never generate new migrations - first schema migration since 1.3; the legacy-upgrade CI job now exercises applying it to a real v1.3 database --- CHANGELOG.md | 3 + .../core/migrations/0005_timezone_choices.py | 24 + pykeg/core/models.py | 10 +- pykeg/core/timezones.py | 459 +----------------- 4 files changed, 41 insertions(+), 455 deletions(-) create mode 100644 pykeg/core/migrations/0005_timezone_choices.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f45cd11..865e9c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ up to date. - **Very old backups can now be restored directly.** `kegbot restore` accepts legacy format-1 backups (created by Kegbot v1.1.x) and upgrades their data in one step; no intermediate 1.2/1.3 install is needed. +- Time zone choices are now derived from the system time zone database, so + newly added zones appear automatically. (Includes a database migration; run + `kegbot upgrade` as usual.) ### Upgrade notes (for existing installs) diff --git a/pykeg/core/migrations/0005_timezone_choices.py b/pykeg/core/migrations/0005_timezone_choices.py new file mode 100644 index 00000000..78287d08 --- /dev/null +++ b/pykeg/core/migrations/0005_timezone_choices.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.16 on 2026-08-03 18:59 + +import pykeg.core.timezones +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0004_version_1_3_part_2"), + ] + + operations = [ + migrations.AlterField( + model_name="kegbotsite", + name="timezone", + field=models.CharField( + choices=pykeg.core.timezones.timezone_choices, + default="UTC", + help_text="Time zone for this system.", + max_length=255, + ), + ), + ] diff --git a/pykeg/core/models.py b/pykeg/core/models.py index 3bc49a53..a738c189 100644 --- a/pykeg/core/models.py +++ b/pykeg/core/models.py @@ -32,7 +32,7 @@ signals, time_series, ) -from pykeg.core.timezones import COMMON_TIMEZONES +from pykeg.core.timezones import timezone_choices from pykeg.core.util import CtoF, get_version from pykeg.util import kbjson, units from pykeg.util.email import build_message @@ -41,12 +41,6 @@ """Django models definition for the kegbot database.""" -# TODO(temporary): COMMON_TIMEZONES is the historical pytz.common_timezones list, -# vendored so this field's `choices` stay byte-for-byte identical and we avoid a -# data-only migration during the 2.0 upgrade. Replace with a zoneinfo-derived -# list (and ship the accompanying migration) as a follow-up. -TIMEZONE_CHOICES = ((z, z) for z in COMMON_TIMEZONES) - logger = logging.getLogger(__name__) @@ -323,7 +317,7 @@ class KegbotSite(models.Model): ) timezone = models.CharField( max_length=255, - choices=TIMEZONE_CHOICES, + choices=timezone_choices, default="UTC", help_text="Time zone for this system.", ) diff --git a/pykeg/core/timezones.py b/pykeg/core/timezones.py index 82f900f1..a6e7d780 100644 --- a/pykeg/core/timezones.py +++ b/pykeg/core/timezones.py @@ -1,449 +1,14 @@ -"""Curated list of common time zones for the KegbotSite.timezone field. +"""Time zone choices for the KegbotSite.timezone field.""" -TODO(temporary): this is the historical pytz.common_timezones list, vendored so -the field's choices stay stable (avoiding a data-only migration) after moving -from pytz to zoneinfo. Time-zone math elsewhere uses stdlib zoneinfo. Replace -with a zoneinfo-derived list (plus its migration) as a follow-up. -""" +from zoneinfo import available_timezones -COMMON_TIMEZONES = [ - "Africa/Abidjan", - "Africa/Accra", - "Africa/Addis_Ababa", - "Africa/Algiers", - "Africa/Asmara", - "Africa/Bamako", - "Africa/Bangui", - "Africa/Banjul", - "Africa/Bissau", - "Africa/Blantyre", - "Africa/Brazzaville", - "Africa/Bujumbura", - "Africa/Cairo", - "Africa/Casablanca", - "Africa/Ceuta", - "Africa/Conakry", - "Africa/Dakar", - "Africa/Dar_es_Salaam", - "Africa/Djibouti", - "Africa/Douala", - "Africa/El_Aaiun", - "Africa/Freetown", - "Africa/Gaborone", - "Africa/Harare", - "Africa/Johannesburg", - "Africa/Juba", - "Africa/Kampala", - "Africa/Khartoum", - "Africa/Kigali", - "Africa/Kinshasa", - "Africa/Lagos", - "Africa/Libreville", - "Africa/Lome", - "Africa/Luanda", - "Africa/Lubumbashi", - "Africa/Lusaka", - "Africa/Malabo", - "Africa/Maputo", - "Africa/Maseru", - "Africa/Mbabane", - "Africa/Mogadishu", - "Africa/Monrovia", - "Africa/Nairobi", - "Africa/Ndjamena", - "Africa/Niamey", - "Africa/Nouakchott", - "Africa/Ouagadougou", - "Africa/Porto-Novo", - "Africa/Sao_Tome", - "Africa/Tripoli", - "Africa/Tunis", - "Africa/Windhoek", - "America/Adak", - "America/Anchorage", - "America/Anguilla", - "America/Antigua", - "America/Araguaina", - "America/Argentina/Buenos_Aires", - "America/Argentina/Catamarca", - "America/Argentina/Cordoba", - "America/Argentina/Jujuy", - "America/Argentina/La_Rioja", - "America/Argentina/Mendoza", - "America/Argentina/Rio_Gallegos", - "America/Argentina/Salta", - "America/Argentina/San_Juan", - "America/Argentina/San_Luis", - "America/Argentina/Tucuman", - "America/Argentina/Ushuaia", - "America/Aruba", - "America/Asuncion", - "America/Atikokan", - "America/Bahia", - "America/Bahia_Banderas", - "America/Barbados", - "America/Belem", - "America/Belize", - "America/Blanc-Sablon", - "America/Boa_Vista", - "America/Bogota", - "America/Boise", - "America/Cambridge_Bay", - "America/Campo_Grande", - "America/Cancun", - "America/Caracas", - "America/Cayenne", - "America/Cayman", - "America/Chicago", - "America/Chihuahua", - "America/Costa_Rica", - "America/Creston", - "America/Cuiaba", - "America/Curacao", - "America/Danmarkshavn", - "America/Dawson", - "America/Dawson_Creek", - "America/Denver", - "America/Detroit", - "America/Dominica", - "America/Edmonton", - "America/Eirunepe", - "America/El_Salvador", - "America/Fort_Nelson", - "America/Fortaleza", - "America/Glace_Bay", - "America/Goose_Bay", - "America/Grand_Turk", - "America/Grenada", - "America/Guadeloupe", - "America/Guatemala", - "America/Guayaquil", - "America/Guyana", - "America/Halifax", - "America/Havana", - "America/Hermosillo", - "America/Indiana/Indianapolis", - "America/Indiana/Knox", - "America/Indiana/Marengo", - "America/Indiana/Petersburg", - "America/Indiana/Tell_City", - "America/Indiana/Vevay", - "America/Indiana/Vincennes", - "America/Indiana/Winamac", - "America/Inuvik", - "America/Iqaluit", - "America/Jamaica", - "America/Juneau", - "America/Kentucky/Louisville", - "America/Kentucky/Monticello", - "America/Kralendijk", - "America/La_Paz", - "America/Lima", - "America/Los_Angeles", - "America/Lower_Princes", - "America/Maceio", - "America/Managua", - "America/Manaus", - "America/Marigot", - "America/Martinique", - "America/Matamoros", - "America/Mazatlan", - "America/Menominee", - "America/Merida", - "America/Metlakatla", - "America/Mexico_City", - "America/Miquelon", - "America/Moncton", - "America/Monterrey", - "America/Montevideo", - "America/Montserrat", - "America/Nassau", - "America/New_York", - "America/Nipigon", - "America/Nome", - "America/Noronha", - "America/North_Dakota/Beulah", - "America/North_Dakota/Center", - "America/North_Dakota/New_Salem", - "America/Nuuk", - "America/Ojinaga", - "America/Panama", - "America/Pangnirtung", - "America/Paramaribo", - "America/Phoenix", - "America/Port-au-Prince", - "America/Port_of_Spain", - "America/Porto_Velho", - "America/Puerto_Rico", - "America/Punta_Arenas", - "America/Rainy_River", - "America/Rankin_Inlet", - "America/Recife", - "America/Regina", - "America/Resolute", - "America/Rio_Branco", - "America/Santarem", - "America/Santiago", - "America/Santo_Domingo", - "America/Sao_Paulo", - "America/Scoresbysund", - "America/Sitka", - "America/St_Barthelemy", - "America/St_Johns", - "America/St_Kitts", - "America/St_Lucia", - "America/St_Thomas", - "America/St_Vincent", - "America/Swift_Current", - "America/Tegucigalpa", - "America/Thule", - "America/Thunder_Bay", - "America/Tijuana", - "America/Toronto", - "America/Tortola", - "America/Vancouver", - "America/Whitehorse", - "America/Winnipeg", - "America/Yakutat", - "America/Yellowknife", - "Antarctica/Casey", - "Antarctica/Davis", - "Antarctica/DumontDUrville", - "Antarctica/Macquarie", - "Antarctica/Mawson", - "Antarctica/McMurdo", - "Antarctica/Palmer", - "Antarctica/Rothera", - "Antarctica/Syowa", - "Antarctica/Troll", - "Antarctica/Vostok", - "Arctic/Longyearbyen", - "Asia/Aden", - "Asia/Almaty", - "Asia/Amman", - "Asia/Anadyr", - "Asia/Aqtau", - "Asia/Aqtobe", - "Asia/Ashgabat", - "Asia/Atyrau", - "Asia/Baghdad", - "Asia/Bahrain", - "Asia/Baku", - "Asia/Bangkok", - "Asia/Barnaul", - "Asia/Beirut", - "Asia/Bishkek", - "Asia/Brunei", - "Asia/Chita", - "Asia/Choibalsan", - "Asia/Colombo", - "Asia/Damascus", - "Asia/Dhaka", - "Asia/Dili", - "Asia/Dubai", - "Asia/Dushanbe", - "Asia/Famagusta", - "Asia/Gaza", - "Asia/Hebron", - "Asia/Ho_Chi_Minh", - "Asia/Hong_Kong", - "Asia/Hovd", - "Asia/Irkutsk", - "Asia/Jakarta", - "Asia/Jayapura", - "Asia/Jerusalem", - "Asia/Kabul", - "Asia/Kamchatka", - "Asia/Karachi", - "Asia/Kathmandu", - "Asia/Khandyga", - "Asia/Kolkata", - "Asia/Krasnoyarsk", - "Asia/Kuala_Lumpur", - "Asia/Kuching", - "Asia/Kuwait", - "Asia/Macau", - "Asia/Magadan", - "Asia/Makassar", - "Asia/Manila", - "Asia/Muscat", - "Asia/Nicosia", - "Asia/Novokuznetsk", - "Asia/Novosibirsk", - "Asia/Omsk", - "Asia/Oral", - "Asia/Phnom_Penh", - "Asia/Pontianak", - "Asia/Pyongyang", - "Asia/Qatar", - "Asia/Qostanay", - "Asia/Qyzylorda", - "Asia/Riyadh", - "Asia/Sakhalin", - "Asia/Samarkand", - "Asia/Seoul", - "Asia/Shanghai", - "Asia/Singapore", - "Asia/Srednekolymsk", - "Asia/Taipei", - "Asia/Tashkent", - "Asia/Tbilisi", - "Asia/Tehran", - "Asia/Thimphu", - "Asia/Tokyo", - "Asia/Tomsk", - "Asia/Ulaanbaatar", - "Asia/Urumqi", - "Asia/Ust-Nera", - "Asia/Vientiane", - "Asia/Vladivostok", - "Asia/Yakutsk", - "Asia/Yangon", - "Asia/Yekaterinburg", - "Asia/Yerevan", - "Atlantic/Azores", - "Atlantic/Bermuda", - "Atlantic/Canary", - "Atlantic/Cape_Verde", - "Atlantic/Faroe", - "Atlantic/Madeira", - "Atlantic/Reykjavik", - "Atlantic/South_Georgia", - "Atlantic/St_Helena", - "Atlantic/Stanley", - "Australia/Adelaide", - "Australia/Brisbane", - "Australia/Broken_Hill", - "Australia/Darwin", - "Australia/Eucla", - "Australia/Hobart", - "Australia/Lindeman", - "Australia/Lord_Howe", - "Australia/Melbourne", - "Australia/Perth", - "Australia/Sydney", - "Canada/Atlantic", - "Canada/Central", - "Canada/Eastern", - "Canada/Mountain", - "Canada/Newfoundland", - "Canada/Pacific", - "Europe/Amsterdam", - "Europe/Andorra", - "Europe/Astrakhan", - "Europe/Athens", - "Europe/Belgrade", - "Europe/Berlin", - "Europe/Bratislava", - "Europe/Brussels", - "Europe/Bucharest", - "Europe/Budapest", - "Europe/Busingen", - "Europe/Chisinau", - "Europe/Copenhagen", - "Europe/Dublin", - "Europe/Gibraltar", - "Europe/Guernsey", - "Europe/Helsinki", - "Europe/Isle_of_Man", - "Europe/Istanbul", - "Europe/Jersey", - "Europe/Kaliningrad", - "Europe/Kiev", - "Europe/Kirov", - "Europe/Lisbon", - "Europe/Ljubljana", - "Europe/London", - "Europe/Luxembourg", - "Europe/Madrid", - "Europe/Malta", - "Europe/Mariehamn", - "Europe/Minsk", - "Europe/Monaco", - "Europe/Moscow", - "Europe/Oslo", - "Europe/Paris", - "Europe/Podgorica", - "Europe/Prague", - "Europe/Riga", - "Europe/Rome", - "Europe/Samara", - "Europe/San_Marino", - "Europe/Sarajevo", - "Europe/Saratov", - "Europe/Simferopol", - "Europe/Skopje", - "Europe/Sofia", - "Europe/Stockholm", - "Europe/Tallinn", - "Europe/Tirane", - "Europe/Ulyanovsk", - "Europe/Uzhgorod", - "Europe/Vaduz", - "Europe/Vatican", - "Europe/Vienna", - "Europe/Vilnius", - "Europe/Volgograd", - "Europe/Warsaw", - "Europe/Zagreb", - "Europe/Zaporozhye", - "Europe/Zurich", - "GMT", - "Indian/Antananarivo", - "Indian/Chagos", - "Indian/Christmas", - "Indian/Cocos", - "Indian/Comoro", - "Indian/Kerguelen", - "Indian/Mahe", - "Indian/Maldives", - "Indian/Mauritius", - "Indian/Mayotte", - "Indian/Reunion", - "Pacific/Apia", - "Pacific/Auckland", - "Pacific/Bougainville", - "Pacific/Chatham", - "Pacific/Chuuk", - "Pacific/Easter", - "Pacific/Efate", - "Pacific/Fakaofo", - "Pacific/Fiji", - "Pacific/Funafuti", - "Pacific/Galapagos", - "Pacific/Gambier", - "Pacific/Guadalcanal", - "Pacific/Guam", - "Pacific/Honolulu", - "Pacific/Kanton", - "Pacific/Kiritimati", - "Pacific/Kosrae", - "Pacific/Kwajalein", - "Pacific/Majuro", - "Pacific/Marquesas", - "Pacific/Midway", - "Pacific/Nauru", - "Pacific/Niue", - "Pacific/Norfolk", - "Pacific/Noumea", - "Pacific/Pago_Pago", - "Pacific/Palau", - "Pacific/Pitcairn", - "Pacific/Pohnpei", - "Pacific/Port_Moresby", - "Pacific/Rarotonga", - "Pacific/Saipan", - "Pacific/Tahiti", - "Pacific/Tarawa", - "Pacific/Tongatapu", - "Pacific/Wake", - "Pacific/Wallis", - "US/Alaska", - "US/Arizona", - "US/Central", - "US/Eastern", - "US/Hawaii", - "US/Mountain", - "US/Pacific", - "UTC", -] + +def timezone_choices(): + """Returns (value, label) choices for every region/city zone, plus UTC. + + Passed as a callable so migrations reference this function rather than + a snapshot of the list; tzdata updates never require a new migration. + """ + zones = {z for z in available_timezones() if "/" in z and not z.startswith("Etc/")} + zones.add("UTC") + return [(z, z) for z in sorted(zones)] From 05c65f6109ec9512d39d81d54573c9efb4674558 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:02:07 +0000 Subject: [PATCH 2/8] backup: resurrect the backup tests, skipped since 2017 - run them on mysql/postgres (there is no sqlite dump implementation) - read db connection params lazily: they were captured at import, before the test runner swaps in the test database, so dump/erase targeted the real database - fix infinite recursion in erase() media cleanup when a media directory has subdirectories --- pykeg/backup/backup.py | 2 +- pykeg/backup/backup_test.py | 24 ++++++++---- pykeg/backup/mysql.py | 73 +++++++++++++++++++--------------- pykeg/backup/postgres.py | 78 ++++++++++++++++++++++--------------- 4 files changed, 104 insertions(+), 73 deletions(-) diff --git a/pykeg/backup/backup.py b/pykeg/backup/backup.py index 77bdec7c..9fdd38a5 100644 --- a/pykeg/backup/backup.py +++ b/pykeg/backup/backup.py @@ -317,7 +317,7 @@ def delete_files(dirname): logger.debug(f"Deleting file: {full_name}") storage.delete(full_name) for subdir in subdirs: - delete_files(dirname) + delete_files(os.path.join(dirname, subdir)) logger.info("Erasing media ...") for media_dir in MEDIA_WHITELIST: diff --git a/pykeg/backup/backup_test.py b/pykeg/backup/backup_test.py index 7dcedab3..00693b39 100644 --- a/pykeg/backup/backup_test.py +++ b/pykeg/backup/backup_test.py @@ -3,7 +3,6 @@ import difflib import os import shutil -import sys import tempfile import unittest @@ -17,13 +16,12 @@ from . import backup +# Dump/restore shell out to engine-specific tools; there is no sqlite +# implementation. +ENGINE_SUPPORTED = "mysql" in backup.engine or "postgres" in backup.engine -def run(cmd, args=[]): - cmdname = cmd.__module__.split(".")[-1] - cmd.run_from_argv([sys.argv[0], cmdname] + args) - -@unittest.skip("backup tests failing") +@unittest.skipUnless(ENGINE_SUPPORTED, "backup requires a mysql or postgres database") class BackupTestCase(TransactionTestCase): def setUp(self): self.temp_storage_location = tempfile.mkdtemp(dir=os.environ.get("DJANGO_TEST_TEMP_DIR")) @@ -100,6 +98,16 @@ def test_backup_contents_same(self): finally: shutil.rmtree(backup_dir) + def normalize_dump(self, contents): + # pg_dump >= 17.6 emits `\restrict ` / `\unrestrict ` + # lines with a token randomized on every dump (CVE-2025-8714), so + # drop them before comparing. + return "".join( + line + for line in contents.splitlines(True) + if not line.startswith(("\\restrict ", "\\unrestrict ")) + ) + def recursive_diff(self, dir1, dir2): dir1_files = set() dir2_files = set() @@ -121,8 +129,8 @@ def recursive_diff(self, dir1, dir2): for relfile in dir1_files: f1_full = os.path.join(dir1, relfile) f2_full = os.path.join(dir2, relfile) - f1 = open(f1_full).read() - f2 = open(f2_full).read() + f1 = self.normalize_dump(open(f1_full).read()) + f2 = self.normalize_dump(open(f2_full).read()) if f1 != f2: message = f'Files not equal: "{f1_full}" and "{f2_full}" differ.' message += "\n" + "".join(difflib.ndiff(f1.splitlines(True), f2.splitlines(True))) diff --git a/pykeg/backup/mysql.py b/pykeg/backup/mysql.py index cf8b79b9..97ce281c 100644 --- a/pykeg/backup/mysql.py +++ b/pykeg/backup/mysql.py @@ -10,31 +10,38 @@ DEFAULT_DB = "default" -# Common command-line arguments -PARAMS = { - "db": settings.DATABASES[DEFAULT_DB].get("NAME"), - "user": settings.DATABASES[DEFAULT_DB].get("USER"), - "password": settings.DATABASES[DEFAULT_DB].get("PASSWORD"), - "host": settings.DATABASES[DEFAULT_DB].get("HOST"), - "port": settings.DATABASES[DEFAULT_DB].get("PORT"), -} - -DEFAULT_ARGS = [] -if PARAMS.get("user"): - DEFAULT_ARGS.append("--user={}".format(PARAMS["user"])) -if PARAMS.get("password"): - DEFAULT_ARGS.append("--password={}".format(PARAMS["password"])) -if PARAMS.get("host"): - DEFAULT_ARGS.append("--host={}".format(PARAMS["host"])) -if PARAMS.get("port"): - DEFAULT_ARGS.append("--port={}".format(PARAMS["port"])) - -# MariaDB 11.4+ clients verify server certificates by default, which fails -# against the self-signed certs MySQL servers auto-generate. Keep TLS but -# skip verification, matching how the Django connection behaves. The -# "loose-" prefix makes clients without this option (Oracle mysql) warn -# instead of exit. -DEFAULT_ARGS.append("--loose-ssl-verify-server-cert=0") + +def db_params(): + """Reads connection parameters lazily: under test, the database name is + swapped for the test database after this module is imported.""" + db = settings.DATABASES[DEFAULT_DB] + return { + "db": db.get("NAME"), + "user": db.get("USER"), + "password": db.get("PASSWORD"), + "host": db.get("HOST"), + "port": db.get("PORT"), + } + + +def common_args(params): + args = [] + if params.get("user"): + args.append("--user={}".format(params["user"])) + if params.get("password"): + args.append("--password={}".format(params["password"])) + if params.get("host"): + args.append("--host={}".format(params["host"])) + if params.get("port"): + args.append("--port={}".format(params["port"])) + + # MariaDB 11.4+ clients verify server certificates by default, which + # fails against the self-signed certs MySQL servers auto-generate. Keep + # TLS but skip verification, matching how the Django connection behaves. + # The "loose-" prefix makes clients without this option (Oracle mysql) + # warn instead of exit. + args.append("--loose-ssl-verify-server-cert=0") + return args def engine_name(): @@ -42,7 +49,8 @@ def engine_name(): def is_installed(): - args = ["mysql", "--batch"] + DEFAULT_ARGS + [PARAMS["db"]] + params = db_params() + args = ["mysql", "--batch"] + common_args(params) + [params["db"]] args += ["-e", "'show tables like \"core_kegbotsite\";'"] cmd = " ".join(args) @@ -53,24 +61,25 @@ def is_installed(): def dump(output_fd): - args = ["mysqldump", "--skip-dump-date", "--single-transaction"] + DEFAULT_ARGS - args.append(PARAMS["db"]) + params = db_params() + args = ["mysqldump", "--skip-dump-date", "--single-transaction"] + common_args(params) + args.append(params["db"]) cmd = " ".join(args) logger.info(cmd) return subprocess.check_call(cmd, stdout=output_fd, shell=True) def restore(input_fd): - args = ["mysql"] + DEFAULT_ARGS - - args.append(PARAMS["db"]) + params = db_params() + args = ["mysql"] + common_args(params) + [params["db"]] cmd = " ".join(args) logger.info(cmd) return subprocess.check_call(cmd, stdin=input_fd, shell=True) def erase(): - args = ["mysql"] + DEFAULT_ARGS + [PARAMS["db"]] + params = db_params() + args = ["mysql"] + common_args(params) + [params["db"]] # Build the sql command. tables = [str(model._meta.db_table) for model in apps.get_models()] diff --git a/pykeg/backup/postgres.py b/pykeg/backup/postgres.py index 355877ed..2cc2f321 100644 --- a/pykeg/backup/postgres.py +++ b/pykeg/backup/postgres.py @@ -10,26 +10,36 @@ DEFAULT_DB = "default" -# Common command-line arguments -PARAMS = { - "db": settings.DATABASES[DEFAULT_DB].get("NAME"), - "user": settings.DATABASES[DEFAULT_DB].get("USER"), - "password": settings.DATABASES[DEFAULT_DB].get("PASSWORD"), - "host": settings.DATABASES[DEFAULT_DB].get("HOST"), - "port": settings.DATABASES[DEFAULT_DB].get("PORT"), -} - -DEFAULT_ARGS = [] -if PARAMS.get("user"): - DEFAULT_ARGS.append("--username={}".format(PARAMS["user"])) -if PARAMS.get("host"): - DEFAULT_ARGS.append("--host={}".format(PARAMS["host"])) -if PARAMS.get("port"): - DEFAULT_ARGS.append("--port={}".format(PARAMS["port"])) - -DEFAULT_ENV = dict(os.environ) -if PARAMS.get("password"): - DEFAULT_ENV["PGPASSWORD"] = PARAMS["password"] + +def db_params(): + """Reads connection parameters lazily: under test, the database name is + swapped for the test database after this module is imported.""" + db = settings.DATABASES[DEFAULT_DB] + return { + "db": db.get("NAME"), + "user": db.get("USER"), + "password": db.get("PASSWORD"), + "host": db.get("HOST"), + "port": db.get("PORT"), + } + + +def common_args(params): + args = [] + if params.get("user"): + args.append("--username={}".format(params["user"])) + if params.get("host"): + args.append("--host={}".format(params["host"])) + if params.get("port"): + args.append("--port={}".format(params["port"])) + return args + + +def common_env(params): + env = dict(os.environ) + if params.get("password"): + env["PGPASSWORD"] = params["password"] + return env def engine_name(): @@ -37,33 +47,37 @@ def engine_name(): def is_installed(): - args = ["psql"] + DEFAULT_ARGS - args += ["-qt", "-c \"select * from pg_tables where schemaname='public';\"", PARAMS["db"]] + params = db_params() + args = ["psql"] + common_args(params) + args += ["-qt", "-c \"select * from pg_tables where schemaname='public';\"", params["db"]] cmd = " ".join(args) logger.info(cmd) - output = subprocess.check_output(cmd, env=DEFAULT_ENV, shell=True, text=True) + output = subprocess.check_output(cmd, env=common_env(params), shell=True, text=True) return "core_" in output def dump(output_fd): - args = ["pg_dump"] + DEFAULT_ARGS - args.append(PARAMS["db"]) + params = db_params() + args = ["pg_dump"] + common_args(params) + args.append(params["db"]) cmd = " ".join(args) logger.info(cmd) - return subprocess.check_call(cmd, stdout=output_fd, env=DEFAULT_ENV, shell=True) + return subprocess.check_call(cmd, stdout=output_fd, env=common_env(params), shell=True) def restore(input_fd): - args = ["psql"] + DEFAULT_ARGS - args.append(PARAMS["db"]) + params = db_params() + args = ["psql"] + common_args(params) + args.append(params["db"]) cmd = " ".join(args) logger.info(cmd) - return subprocess.check_call(cmd, stdin=input_fd, env=DEFAULT_ENV, shell=True) + return subprocess.check_call(cmd, stdin=input_fd, env=common_env(params), shell=True) def erase(): - args = ["psql"] + DEFAULT_ARGS - args += [PARAMS["db"], "-c 'drop schema public cascade; create schema public;'"] + params = db_params() + args = ["psql"] + common_args(params) + args += [params["db"], "-c 'drop schema public cascade; create schema public;'"] cmd = " ".join(args) logger.info(cmd) - subprocess.check_call(cmd, env=DEFAULT_ENV, shell=True) + subprocess.check_call(cmd, env=common_env(params), shell=True) From a16ef1c1c41ef1f1c6ae1c4b2dd40c5f42f2f4f0 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:02:46 +0000 Subject: [PATCH 3/8] core: remove the dead flake8 test - skipped since the flake8 era; ruff runs as its own CI step - drop the stale REUSE_DB pytest option (django-nose relic) that warned on every test run --- pykeg/core/tests.py | 29 ----------------------------- setup.cfg | 1 - 2 files changed, 30 deletions(-) delete mode 100644 pykeg/core/tests.py diff --git a/pykeg/core/tests.py b/pykeg/core/tests.py deleted file mode 100644 index 9db15774..00000000 --- a/pykeg/core/tests.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Generic unittests.""" - -import os -import subprocess -import unittest -from importlib import import_module - -from django.test import TestCase - - -def path_for_import(name): - """ - Returns the directory path for the given package or module. - """ - return os.path.dirname(os.path.abspath(import_module(name).__file__)) - - -@unittest.skip("lint tests failing") -class CoreTests(TestCase): - def test_flake8(self): - root_path = path_for_import("pykeg") - config_file = os.path.join(root_path, "setup.cfg") - command = f"flake8 --config={config_file} {root_path}" - try: - subprocess.check_output(command.split()) - except subprocess.CalledProcessError as e: - print(f"command: {command}") - print(e.output) - self.fail(f"flake8 failed with return code {e.returncode}.") diff --git a/setup.cfg b/setup.cfg index 6bbf076e..a60f7b0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,4 +3,3 @@ addopts = -p pykeg.test.plugin -ra --reuse-db --lfnf=all testpaths = pykeg/ python_files = tests.py test_*.py *_tests.py *_test.py DJANGO_SETTINGS_MODULE = pykeg.settings -REUSE_DB = 1 From 840511c19d9474d413e6414ab50269f0f3fe22be Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:03:54 +0000 Subject: [PATCH 4/8] core: migration: add default ordering to hardware models - Controller, FlowMeter, FlowToggle, and ThermoSensor lists (api and kegadmin) had no ordering, so postgres returned them in arbitrary heap order - no-op at the database level (AlterModelOptions only) --- .../core/migrations/0006_hardware_ordering.py | 29 +++++++++++++++++++ pykeg/core/models.py | 8 +++++ 2 files changed, 37 insertions(+) create mode 100644 pykeg/core/migrations/0006_hardware_ordering.py diff --git a/pykeg/core/migrations/0006_hardware_ordering.py b/pykeg/core/migrations/0006_hardware_ordering.py new file mode 100644 index 00000000..f100af05 --- /dev/null +++ b/pykeg/core/migrations/0006_hardware_ordering.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.16 on 2026-08-03 19:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0005_timezone_choices"), + ] + + operations = [ + migrations.AlterModelOptions( + name="controller", + options={"ordering": ("name",)}, + ), + migrations.AlterModelOptions( + name="flowmeter", + options={"ordering": ("controller", "port_name")}, + ), + migrations.AlterModelOptions( + name="flowtoggle", + options={"ordering": ("controller", "port_name")}, + ), + migrations.AlterModelOptions( + name="thermosensor", + options={"ordering": ("raw_name",)}, + ), + ] diff --git a/pykeg/core/models.py b/pykeg/core/models.py index a738c189..128eb2bc 100644 --- a/pykeg/core/models.py +++ b/pykeg/core/models.py @@ -807,6 +807,9 @@ def get_from_meter_name(cls, meter_name): class Controller(models.Model): + class Meta: + ordering = ("name",) + name = models.CharField( max_length=128, unique=True, help_text="Identifying name for this device; must be unique." ) @@ -824,6 +827,7 @@ def __str__(self): class FlowMeter(models.Model): class Meta: unique_together = ("controller", "port_name") + ordering = ("controller", "port_name") controller = models.ForeignKey( Controller, @@ -889,6 +893,7 @@ def get_from_meter_name(cls, meter_name): class FlowToggle(models.Model): class Meta: unique_together = ("controller", "port_name") + ordering = ("controller", "port_name") controller = models.ForeignKey( Controller, @@ -1847,6 +1852,9 @@ def AssignSessionForDrink(cls, drink): class ThermoSensor(models.Model): + class Meta: + ordering = ("raw_name",) + raw_name = models.CharField(max_length=256) nice_name = models.CharField(max_length=128) From 90e9e04f0e4e87f32d628365ea698a0f5489d194 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:17:24 +0000 Subject: [PATCH 5/8] kegadmin: adopt the Django 6 URLField default scheme - schemeless producer urls now assume https; silences the last deprecation warning in the test run --- pykeg/web/kegadmin/forms.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pykeg/web/kegadmin/forms.py b/pykeg/web/kegadmin/forms.py index 3a50cc12..8962f819 100644 --- a/pykeg/web/kegadmin/forms.py +++ b/pykeg/web/kegadmin/forms.py @@ -458,6 +458,10 @@ class Meta: class BeverageProducerForm(forms.ModelForm): + # Django 6.0 changes the default scheme to https; set it explicitly to + # silence the transition warning. + url = forms.URLField(assume_scheme="https", required=False, help_text="Brewer's home page") + class Meta: model = models.BeverageProducer fields = ( From b64f27a1b833970af2936656b039bdce2dd08812 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:18:02 +0000 Subject: [PATCH 6/8] docker: refresh the compose files - example now pulls ghcr.io/kegbot/server:latest (the published image; 'stable' predates the current release tagging) - pin mysql:8; mysql:latest is now 9.x - wait for mysql to be healthy before starting the app services - drop the deprecated compose 'version' key and the stale KEGBOT_DEBUG variable --- docker-compose.yml | 23 +++++++++++++++++++---- docs/source/docker-compose.example.yml | 25 ++++++++++++++++++++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e35cd0a8..d6381c2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,8 +4,6 @@ # make the appropriate changes to `docs/source/docker-compose.example.yml` # as well. -version: '3' - services: kegbot: image: kegbot-server @@ -24,6 +22,11 @@ services: DATABASE_URL: mysql://kegbot_dev:changeme@mysql/kegbot_dev KEGBOT_ENV: "debug" KEGBOT_SECRET_KEY: "changeme" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started workers: image: kegbot-server @@ -37,17 +40,29 @@ services: environment: REDIS_URL: redis://redis:6379/0 DATABASE_URL: mysql://kegbot_dev:changeme@mysql/kegbot_dev - KEGBOT_DEBUG: "true" + KEGBOT_ENV: "debug" KEGBOT_SECRET_KEY: "changeme" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started mysql: - image: mysql:latest + image: mysql:8 restart: always environment: MYSQL_ROOT_PASSWORD: 'changeme' MYSQL_USER: 'kegbot_dev' MYSQL_PASSWORD: 'changeme' MYSQL_DATABASE: 'kegbot_dev' + healthcheck: + # Ping over TCP: during init the image runs a socket-only temporary + # server that would pass a plain socket ping too early. + test: ["CMD", "mysqladmin", "ping", "-h127.0.0.1", "--silent"] + interval: 5s + timeout: 5s + retries: 30 tmpfs: - /tmp - /var/tmp diff --git a/docs/source/docker-compose.example.yml b/docs/source/docker-compose.example.yml index 61efa2d3..9af9f016 100644 --- a/docs/source/docker-compose.example.yml +++ b/docs/source/docker-compose.example.yml @@ -1,8 +1,6 @@ -version: '3' - services: kegbot: - image: ghcr.io/kegbot/server:stable + image: ghcr.io/kegbot/server:latest restart: unless-stopped command: run_server ports: @@ -17,9 +15,14 @@ services: DATABASE_URL: mysql://kegbot:changeme@mysql/kegbot KEGBOT_ENV: "debug" KEGBOT_SECRET_KEY: "changeme" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started workers: - image: ghcr.io/kegbot/server:stable + image: ghcr.io/kegbot/server:latest restart: unless-stopped command: run_workers volumes: @@ -32,15 +35,27 @@ services: DATABASE_URL: mysql://kegbot:changeme@mysql/kegbot KEGBOT_ENV: "debug" KEGBOT_SECRET_KEY: "changeme" + depends_on: + mysql: + condition: service_healthy + redis: + condition: service_started mysql: - image: mysql:latest + image: mysql:8 restart: always environment: MYSQL_ROOT_PASSWORD: 'changeme' MYSQL_USER: 'kegbot' MYSQL_PASSWORD: 'changeme' MYSQL_DATABASE: 'kegbot' + healthcheck: + # Ping over TCP: during init the image runs a socket-only temporary + # server that would pass a plain socket ping too early. + test: ["CMD", "mysqladmin", "ping", "-h127.0.0.1", "--silent"] + interval: 5s + timeout: 5s + retries: 30 tmpfs: - /tmp - /var/tmp From f16cdc3b6c5a806692b3073548d41185acd64041 Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:20:16 +0000 Subject: [PATCH 7/8] docs: refresh for v2 - developers guide now describes the uv/ruff/pre-commit workflow - add the 2.0.0 section to the published changelog - document the backup/restore commands and the direct v1.1.x legacy-restore path - modernize 'docker compose' invocations and the sample output --- docs/source/commands.rst | 17 +++++++-- docs/source/developers.rst | 56 ++++++++++++++++++------------ docs/source/install.rst | 20 +++++------ docs/source/overview.rst | 2 +- docs/source/releases/changelog.rst | 35 +++++++++++++++++++ docs/source/settings.rst | 2 +- docs/source/upgrade.rst | 28 ++++++++++++--- 7 files changed, 118 insertions(+), 42 deletions(-) diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 26e874b0..3924420b 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -9,12 +9,12 @@ This section describes commonly-used commands that are available from the Running commands ---------------- -When using ``docker-compose``, you can run the ``kegbot`` command line using +When using ``docker compose``, you can run the ``kegbot`` command line using the following general invocation: .. code-block:: console - $ docker-compose run kegbot [.. additional args ..] + $ docker compose run kegbot [.. additional args ..] For an example, see the :ref:`upgrading` section. @@ -40,11 +40,22 @@ Kegbot Server. Change the password of the given user. +.. data:: backup + + Creates a zipfile backup of the database and stored media. The backup is + written to the ``backups/`` folder of the site's media storage. + +.. data:: restore + + Restores a backup zipfile into a fresh (erased) system. Backups created + by Kegbot v1.1.x are upgraded automatically during restore; see + :ref:`upgrade-legacy`. + Internal commands ~~~~~~~~~~~~~~~~~ -These commands are what makes the Kegbot Server run. When using ``docker-compose``, +These commands are what makes the Kegbot Server run. When using ``docker compose``, you should not need to call these commands directly, as they're invoked by that configuration when needed. diff --git a/docs/source/developers.rst b/docs/source/developers.rst index e7325e90..2dbbba62 100644 --- a/docs/source/developers.rst +++ b/docs/source/developers.rst @@ -10,44 +10,59 @@ Local environment ----------------- Most likely, you'll want to run kegbot locally (outside of Docker) while -developing. We use `Poetry` to manage the Python environment. Create -your development environment this way: +developing. We use `uv `_ to manage the Python +environment. Create your development environment this way: .. code-block:: console - $ poetry install + $ uv sync --all-groups -This will fetch and install all dependencies, and create a virtual Python -environment. +This will fetch and install all dependencies into a virtual Python +environment at ``.venv``. -Whenever you want to run code or tests, step into a development shell: +A few settings are required even in development. A minimal configuration, +using sqlite and a local redis: .. code-block:: console - $ poetry shell - (kegbot-server) $ ./bin/kegbot version - 1.3.0 + $ export KEGBOT_SECRET_KEY=changeme + $ export DATABASE_URL=sqlite:///kegbot-dev.db + $ export REDIS_URL=redis://localhost:6379/0 + +Run the server, or any other command, through ``uv run``: + +.. code-block:: console + + $ uv run bin/kegbot version + $ uv run bin/kegbot migrate + $ uv run bin/kegbot run_server Running tests ------------- -We use `pytest` to run tests. Run all tests this way: +We use `pytest` to run tests. The test suite runs against sqlite and needs +no redis server: .. code-block:: console - (kegbot-server) $ pytest - + $ uv run pytest -Code format ------------ +Code format and lint +-------------------- -We use `black` to format all code. Run it this way: +We use `ruff` to format and lint all code: .. code-block:: console - $ poetry shell - (kegbot-server) $ black pykeg/ + $ uv run ruff format + $ uv run ruff check +To run these checks automatically before each commit, install the +`pre-commit` hooks: + +.. code-block:: console + + $ uv run pre-commit install Building docs ------------- @@ -56,8 +71,5 @@ We use `Sphinx` to build docs. You can create them this way: .. code-block:: console - $ poetry shell - (kegbot-server) $ cd docs - (kegbot-server) $ make html - (kegbot-server) $ open build/html/index.html - + $ uv run sphinx-build -b html docs/source docs/build/html + $ open docs/build/html/index.html diff --git a/docs/source/install.rst b/docs/source/install.rst index fad85243..709fa875 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -7,7 +7,7 @@ Prerequisites ------------- Kegbot Server is installed and supported through `Docker `_ -and `docker-compose `_, which are available for Mac, +and `Docker Compose `_, which are available for Mac, Windows, and Linux. Ensure you have both of these installed before continuing. @@ -27,9 +27,9 @@ Create the config file ---------------------- Kegbot and its essential services will be configured and launched using the -``docker-compose`` tool and a corresponding config file, ``docker-compose.yml``. +``docker compose`` tool and a corresponding config file, ``docker-compose.yml``. -Create a new filed called ``docker-compose.yml`` starting with the following contents: +Create a new file called ``docker-compose.yml`` starting with the following contents: .. include:: ./docker-compose.example.yml :literal: @@ -53,10 +53,10 @@ To this:: Start the services ------------------ -Now, ask ``docker-compose`` to launch these services. We will launch them in the +Now, ask ``docker compose`` to launch these services. We will launch them in the foreground:: - $ docker-compose up + $ docker compose up This may take a while, as the Docker system works to download the images it needs. Eventually, you should start seeing a series of output like the following:: @@ -113,7 +113,7 @@ them in the background: .. code-block:: console - $ docker-compose up -d + $ docker compose up -d This time, you should see only a few brief lines of output: @@ -125,13 +125,13 @@ This time, you should see only a few brief lines of output: ⠿ Container kegbot-server-redis-1 Started 0.6s ⠿ Container kegbot-server-workers-1 Started 0.6s -You can verify everything is running with the ``docker-compose ps`` command: +You can verify everything is running with the ``docker compose ps`` command: .. code-block:: console - $ docker-compose ps + $ docker compose ps NAME COMMAND SERVICE STATUS PORTS - kegbot-server-kegbot-1 "gunicorn pykeg.web.…" kegbot running 0.0.0.0:8000->8000/tcp + kegbot-server-kegbot-1 "/usr/local/sbin/kegbo…" kegbot running 0.0.0.0:8000->8000/tcp kegbot-server-mysql-1 "docker-entrypoint.s…" mysql running 3306/tcp, 33060/tcp kegbot-server-redis-1 "docker-entrypoint.s…" redis running 6379/tcp - kegbot-server-workers-1 "bin/kegbot run_work…" workers running 8000/tcp + kegbot-server-workers-1 "/usr/local/sbin/kegbo…" workers running 8000/tcp diff --git a/docs/source/overview.rst b/docs/source/overview.rst index f219cb16..a7c4bdd5 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -10,7 +10,7 @@ installing a new Kegbot server. Prerequisites ------------- -Kegbot Server requires `Docker `_ to run, and +Kegbot Server requires `Docker `_ to run, and runs on any operating system that supports Docker. We have tested these instructions on Linux and Mac OS X machines. diff --git a/docs/source/releases/changelog.rst b/docs/source/releases/changelog.rst index 79d45252..7b0c5fc9 100644 --- a/docs/source/releases/changelog.rst +++ b/docs/source/releases/changelog.rst @@ -6,6 +6,41 @@ Changelog **Upgrade Procedure:** Please follow :ref:`upgrading` for general upgrade steps. +Version 2.0.0 (unreleased) +-------------------------- + +A modernization release. The runtime, framework, and toolchain were all +brought up to date. + +**Highlights** + +* Python 3.14 is now required (was 3.10). +* Django 5.2 LTS (was 3.2). +* Web server switched from gunicorn/gevent to waitress. +* Packaging moved from Poetry to uv; linting and formatting moved to ruff. +* protobuf upgraded to the 6.x series. +* Docker images are published to ``ghcr.io/kegbot/server``. +* **Very old backups can now be restored directly.** ``kegbot restore`` + accepts legacy format-1 backups (created by Kegbot v1.1.x) and upgrades + their data in one step; no intermediate 1.2/1.3 install is needed. +* Time zone choices are now derived from the system time zone database. + +**Upgrade notes** + +* **Everyone is logged out once.** Sessions now use the JSON serializer, so + existing session cookies are invalidated on upgrade. Users simply log in + again. +* **Background jobs enqueue on database commit.** Stats and notification + jobs are handed to the worker only after the surrounding database + transaction commits. Ensure ``run_workers`` is running (unchanged) to + process them. +* ``run_gunicorn`` was removed. Use ``kegbot run_server`` (now waitress). +* The Docker image no longer publishes a ``linux/arm/v7`` variant (amd64 and + arm64 only). +* The legacy gflags-based Python API client was removed from the server + package; it lives in the separate kegbot-api project. + + Version 1.3.0 (2022-08-10) -------------------------- diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 5bd66a6d..e1cd4cc6 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -15,7 +15,7 @@ This section lists all settings (environment variables) the server recognizes. Required settings ~~~~~~~~~~~~~~~~~ -These settings have no default and must be set by you. (When you use ``docker-compose`` +These settings have no default and must be set by you. (When you use ``docker compose`` with the example configuration in these docs, all required values will be set.) .. data:: DATABASE_URL diff --git a/docs/source/upgrade.rst b/docs/source/upgrade.rst index ed0a3ea7..040511c3 100644 --- a/docs/source/upgrade.rst +++ b/docs/source/upgrade.rst @@ -19,28 +19,28 @@ Step 1 First, ensure the system has been stopped:: - $ docker-compose down + $ docker compose down Step 2 ~~~~~~ Next, fetch the latest images:: - $ docker-compose pull + $ docker compose pull Step 3 ~~~~~~ Next, restart just the database and redis:: - $ docker-compose up -d mysql redis + $ docker compose up -d mysql redis Step 4 ~~~~~~ Next, run the upgrade command:: - $ docker-compose run kegbot upgrade + $ docker compose run kegbot upgrade You will see upgrade progress, followed by the message ``Upgrade complete!``. If you see the message ``Version is already installed.``, then no upgrade @@ -51,4 +51,22 @@ Step 5 Finally, restart the containers:: - $ docker-compose up -d kegbot workers + $ docker compose up -d kegbot workers + +.. _upgrade-legacy: + +Upgrading from very old versions +-------------------------------- + +Backups created by Kegbot v1.1.x (the "format 1" zipfile format) can be +restored directly into this version; no intermediate v1.2/v1.3 installation +is needed. + +Place the backup zipfile somewhere the container can read it — for example, +your ``kegbot-data`` directory — then, starting from a fresh (empty) +database:: + + $ docker compose run kegbot restore /kegbot-data/my-old-backup.zip + +The restore loads the old data, upgrades it in place, and regenerates +statistics. From 8edd06c680dbb0d79c3a702cb36bd8e74aa6f4dc Mon Sep 17 00:00:00 2001 From: mike wakerly Date: Mon, 3 Aug 2026 19:21:13 +0000 Subject: [PATCH 8/8] docs: make the rst changelog the single source of truth - CHANGELOG.md is now just a pointer; the changelog was already maintained in docs/source/releases/changelog.rst --- CHANGELOG.md | 39 +++--------------------------- docs/source/releases/changelog.rst | 3 ++- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 865e9c91..e8167f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,39 +1,6 @@ # Changelog -You can [view the published changelog here](https://docs.kegbot.org/projects/kegbot-server/en/latest/releases/changelog.html). +The changelog is maintained at +[`docs/source/releases/changelog.rst`](docs/source/releases/changelog.rst). -## Current version (unreleased) - -A modernization release. The runtime, framework, and toolchain were all brought -up to date. - -### Highlights - -- **Python 3.14** is now required (was 3.10). -- **Django 5.2 LTS** (was 3.2). -- Web server switched from **gunicorn/gevent** to **waitress**. -- Packaging moved from **Poetry** to **uv**; linting/formatting moved to **ruff**; - pre-flight checks run via **pre-commit**. -- protobuf upgraded to the 6.x series. -- Docker image rebuilt on `python:3.14-slim` with uv. -- **Very old backups can now be restored directly.** `kegbot restore` accepts - legacy format-1 backups (created by Kegbot v1.1.x) and upgrades their data in - one step; no intermediate 1.2/1.3 install is needed. -- Time zone choices are now derived from the system time zone database, so - newly added zones appear automatically. (Includes a database migration; run - `kegbot upgrade` as usual.) - -### Upgrade notes (for existing installs) - -- **Everyone is logged out once.** Sessions now use the JSON serializer (Django - removed the pickle serializer), so existing session cookies are invalidated on - upgrade. Users simply log in again. -- **Background jobs enqueue on database commit.** Stats/notification jobs are now - handed to the worker only after the surrounding DB transaction commits. Ensure - `run_workers` is running (unchanged) to process them. -- **`run_gunicorn` was removed.** Use `kegbot run_server` (now waitress). Tune - with `--waitress_options` (e.g. `--threads=8`); `$PORT` is still honored. -- The Docker image no longer publishes a `linux/arm/v7` variant (amd64 + arm64 - only). -- The legacy gflags-based Python API client was removed from the server package; - it lives in the separate kegbot-api project. +You can also [view it on the web](https://docs.kegbot.org/projects/kegbot-server/en/latest/releases/changelog.html). diff --git a/docs/source/releases/changelog.rst b/docs/source/releases/changelog.rst index 7b0c5fc9..0cd3ab66 100644 --- a/docs/source/releases/changelog.rst +++ b/docs/source/releases/changelog.rst @@ -19,7 +19,8 @@ brought up to date. * Web server switched from gunicorn/gevent to waitress. * Packaging moved from Poetry to uv; linting and formatting moved to ruff. * protobuf upgraded to the 6.x series. -* Docker images are published to ``ghcr.io/kegbot/server``. +* Docker image rebuilt on ``python:3.14-slim`` with uv; images are published + to ``ghcr.io/kegbot/server``. * **Very old backups can now be restored directly.** ``kegbot restore`` accepts legacy format-1 backups (created by Kegbot v1.1.x) and upgrades their data in one step; no intermediate 1.2/1.3 install is needed.