From d12f9d91d1640abaa5deb042cc11c54ea499c4c5 Mon Sep 17 00:00:00 2001 From: Jeltz Date: Mon, 10 Feb 2025 01:38:21 +0100 Subject: [PATCH] WIP: Add indico profile + required packages Signed-off-by: Jeltz --- hive.nix | 1 + pkgs/flask-multipass/default.nix | 57 ++ pkgs/flask-pluginengine/default.nix | 41 ++ pkgs/flask-webpackext/default.nix | 41 ++ pkgs/indico-fonts/default.nix | 31 + pkgs/indico/default.nix | 174 ++++++ pkgs/indico/remove_marshmallow_enum.patch | 678 ++++++++++++++++++++++ pkgs/pynpm/default.nix | 34 ++ pkgs/pywebpack/default.nix | 41 ++ pkgs/wtforms-dateutil/default.nix | 36 ++ pkgs/wtforms-sqlalchemy/default.nix | 37 ++ profiles/indico.nix | 116 ++++ secrets/discourse-key-base.age | 60 +- secrets/discourse-mail-password.age | 59 +- secrets/matrix-shared-secret.age | Bin 1825 -> 1825 bytes secrets/mautrix-telegram.age | 61 +- secrets/vaultwarden-secrets.age | Bin 1809 -> 1809 bytes 17 files changed, 1377 insertions(+), 90 deletions(-) create mode 100644 pkgs/flask-multipass/default.nix create mode 100644 pkgs/flask-pluginengine/default.nix create mode 100644 pkgs/flask-webpackext/default.nix create mode 100644 pkgs/indico-fonts/default.nix create mode 100644 pkgs/indico/default.nix create mode 100644 pkgs/indico/remove_marshmallow_enum.patch create mode 100644 pkgs/pynpm/default.nix create mode 100644 pkgs/pywebpack/default.nix create mode 100644 pkgs/wtforms-dateutil/default.nix create mode 100644 pkgs/wtforms-sqlalchemy/default.nix create mode 100644 profiles/indico.nix diff --git a/hive.nix b/hive.nix index f1c103d..8c4f333 100644 --- a/hive.nix +++ b/hive.nix @@ -165,6 +165,7 @@ in imports = [ ./profiles/vm.nix + ./profiles/indico.nix ]; }; } diff --git a/pkgs/flask-multipass/default.nix b/pkgs/flask-multipass/default.nix new file mode 100644 index 0000000..2a6f410 --- /dev/null +++ b/pkgs/flask-multipass/default.nix @@ -0,0 +1,57 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + hatchling, + flask, + blinker, + authlib, + requests, + flask-wtf, + python-ldap, + python3-saml, + sqlalchemy, +}: + +buildPythonPackage rec { + pname = "flask-multipass"; + version = "0.6"; + pyproject = true; + + src = fetchFromGitHub { + owner = "indico"; + repo = "flask-multipass"; + rev = "refs/tags/v${version}"; + hash = "sha256-1SfyCpa1j7a1emhruVLBtXwDr6BisnvUnTA1ioQmGzQ="; + }; + + build-system = [ hatchling ]; + + dependencies = [ + flask + blinker + ]; + + optional-dependencies = { + authlib = [ + authlib + requests + ]; + ldap = [ + flask-wtf + python-ldap + ]; + saml = [ python3-saml ]; + sqlalchemy = [ + sqlalchemy + flask-wtf + ]; + }; + + meta = { + description = "A pluggable solution for multi-backend authentication with Flask"; + homepage = "https://github.com/indico/flask-multipass"; + changelog = "https://github.com/indico/flask-multipass/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/flask-pluginengine/default.nix b/pkgs/flask-pluginengine/default.nix new file mode 100644 index 0000000..a266f0a --- /dev/null +++ b/pkgs/flask-pluginengine/default.nix @@ -0,0 +1,41 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + flask, + jinja2, + blinker, + importlib-metadata, +}: + +buildPythonPackage rec { + pname = "flask-pluginengine"; + version = "0.5"; + pyproject = true; + + src = fetchFromGitHub { + owner = "indico"; + repo = "flask-pluginengine"; + rev = "refs/tags/v${version}"; + hash = "sha256-O41z1GYA4pau6UP2rw4aU4SMr3sXsn4yd+yMvVNwT0k="; + }; + + build-system = [ setuptools ]; + + dependencies = [ + flask + jinja2 + blinker + importlib-metadata + ]; + + pythonImportChecks = [ "flask_pluginengine" ]; + + meta = { + description = "A simple plugin system for Flask applications"; + homepage = "https://github.com/indico/flask-pluginengine"; + changelog = "https://github.com/indico/flask-pluginengine/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/flask-webpackext/default.nix b/pkgs/flask-webpackext/default.nix new file mode 100644 index 0000000..87361d3 --- /dev/null +++ b/pkgs/flask-webpackext/default.nix @@ -0,0 +1,41 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + wheel, + babel, + flask, + pywebpack, +}: + +buildPythonPackage rec { + pname = "flask-webpackext"; + version = "2.0.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "inveniosoftware"; + repo = "flask-webpackext"; + rev = "refs/tags/v${version}"; + hash = "sha256-jGYUsmj52dsu9hR4TEDVt/jX3mt5ForYbhwi2t4DaRw="; + }; + + build-system = [ + setuptools + wheel + babel + ]; + + dependencies = [ + flask + pywebpack + ]; + + meta = { + description = "Webpack integration for Flask"; + homepage = "https://github.com/inveniosoftware/flask-webpackext"; + changelog = "https://github.com/inveniosoftware/flask-webpackext/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/indico-fonts/default.nix b/pkgs/indico-fonts/default.nix new file mode 100644 index 0000000..39fdcb7 --- /dev/null +++ b/pkgs/indico-fonts/default.nix @@ -0,0 +1,31 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, +}: + +buildPythonPackage rec { + pname = "indico-fonts"; + version = "1.2"; + pyproject = true; + + src = fetchFromGitHub { + owner = "indico"; + repo = "indico-fonts"; + rev = "refs/tags/v${version}"; + hash = "sha256-tqpsmthcg9BXxoM/cii+dtXzw0vgu8sek/F1cVNTu+8="; + }; + + build-system = [ setuptools ]; + + meta = { + description = "Indico binary fonts"; + homepage = "https://github.com/indico/indico-fonts"; + license = [ + lib.licenses.ofl + lib.licenses.arphicpl + # TODO indico_fonts/LICENSE-efont + ]; + }; +} diff --git a/pkgs/indico/default.nix b/pkgs/indico/default.nix new file mode 100644 index 0000000..d00560e --- /dev/null +++ b/pkgs/indico/default.nix @@ -0,0 +1,174 @@ +{ + lib, + fetchFromGitHub, + fetchPypi, + python3, +}: + +let + python = python3.override { + self = python; + packageOverrides = self: super: { + celery = super.celery.overridePythonAttrs rec { + optional-dependencies.redis = [ self.redis ]; + }; + # Version 3.0.5 is the last one to support SQLAlchemy 1.4 + flask-multipass = self.callPackage ../flask-multipass { }; + flask-pluginengine = self.callPackage ../flask-pluginengine { }; + flask-sqlalchemy = super.flask-sqlalchemy.overridePythonAttrs rec { + version = "3.0.5"; + src = fetchPypi { + pname = "flask_sqlalchemy"; + inherit version; + hash = "sha256-xXZeWMoUVAG1IQbA9GF4VpJDxdolVWviwjHsxghnxbE="; + }; + }; + flask-webpackext = self.callPackage ../flask-webpackext { }; + indico-fonts = self.callPackage ../indico-fonts { }; + lxml = super.lxml.overridePythonAttrs rec { + optional-dependencies.html5 = [ self.html5lib ]; + }; + pynpm = self.callPackage ../pynpm { }; + pywebpack = self.callPackage ../pywebpack { }; + sentry-sdk = super.sentry-sdk_1; + sqlalchemy = super.sqlalchemy_1_4; + wtforms-dateutil = self.callPackage ../wtforms-dateutil { }; + wtforms-sqlalchemy = self.callPackage ../wtforms-sqlalchemy { }; + }; + }; +in +python.pkgs.buildPythonApplication rec { + pname = "indico"; + version = "3.3.5"; + pyproject = true; + + disabled = python.pkgs.pythonOlder "3.12"; + + src = fetchFromGitHub { + owner = "indico"; + repo = "indico"; + rev = "refs/tags/v${version}"; + hash = "sha256-OX5tqeIjd7Lb5XfvFFKcYb9Dbf5Z9QLXlVTepTpeOMU="; + }; + + build-system = with python.pkgs; [ + hatchling + hatch-requirements-txt + ]; + + patches = [ + ./remove_marshmallow_enum.patch + ]; + + postPatch = '' + # See JoshData/python-email-validator#90 + substituteInPlace requirements.in \ + --replace "email-validator<1.3.0" "email-validator!=1.3.0" + # Botocore doesn't seem to be used anymore + substituteInPlace requirements.in --replace "urllib3<2" "urllib3" + # Seems fixed + substituteInPlace requirements.in --replace "pydyf<0.10" "pydyf" + # Remove transitive dependencies and unnecessary version pins + cp requirements.in requirements.txt + ''; + + dependencies = + with python.pkgs; + [ + alembic + authlib + babel + bcrypt + bleach + blinker + captcha + celery + certifi + click + colorclass + distro + email-validator + feedgen + flask-babel + flask-caching + flask-cors + flask-limiter + flask-marshmallow + flask-migrate + flask-multipass + flask-pluginengine + flask-sqlalchemy + flask-webpackext + flask-wtf + flask + google-auth + hiredis + html2text + icalendar + indico-fonts + ipython + itsdangerous + jinja2 + jsonschema + lxml + markdown + markupsafe + marshmallow-dataclass + marshmallow-oneofschema + marshmallow-sqlalchemy + marshmallow + packaging + pillow + prompt-toolkit + psycopg2 + pycountry + pydyf + pygments + pypdf + python-dateutil + pytz + pywebpack + pyyaml + qrcode + redis + reportlab + requests + sentry-sdk + simplejson + speaklater + sqlalchemy + terminaltables + translitcodec + ua-parser + urllib3 + wallet-py3k + weasyprint + webargs + werkzeug + wtforms + wtforms-dateutil + wtforms-sqlalchemy + xlsxwriter + ] + ++ bleach.optional-dependencies.css + ++ celery.optional-dependencies.redis + ++ qrcode.optional-dependencies.pil + ++ lxml.optional-dependencies.html5 + ++ redis.optional-dependencies.hiredis + ++ sentry-sdk_1.optional-dependencies.flask + ++ sentry-sdk_1.optional-dependencies.celery + ++ sentry-sdk_1.optional-dependencies.sqlalchemy + ++ sentry-sdk_1.optional-dependencies.pure_eval + ++ wtforms.optional-dependencies.email; + + passthru = { + inherit python; + }; + + meta = { + description = "Full-featured conferency lifecycle management and meeting/lecture scheduling tool"; + homepage = "https://getindico.io/"; + changelog = "https://docs.getindico.io/en/stable/changelog/"; + license = lib.licenses.mit; + }; +} diff --git a/pkgs/indico/remove_marshmallow_enum.patch b/pkgs/indico/remove_marshmallow_enum.patch new file mode 100644 index 0000000..ba2fdfd --- /dev/null +++ b/pkgs/indico/remove_marshmallow_enum.patch @@ -0,0 +1,678 @@ +diff --git a/indico/core/marshmallow.py b/indico/core/marshmallow.py +index d3718a5..e51ba0a 100644 +--- a/indico/core/marshmallow.py ++++ b/indico/core/marshmallow.py +@@ -10,7 +10,6 @@ from inspect import getmro + from flask_marshmallow import Marshmallow + from flask_marshmallow.sqla import SQLAlchemyAutoSchemaOpts + from marshmallow import fields, post_dump, post_load, pre_load +-from marshmallow_enum import EnumField + from marshmallow_sqlalchemy import ModelConverter + from marshmallow_sqlalchemy import SQLAlchemyAutoSchema as MSQLASQLAlchemyAutoSchema + from sqlalchemy.orm import ColumnProperty +@@ -33,7 +32,7 @@ class IndicoModelConverter(ModelConverter): + SQLA_TYPE_MAPPING = ModelConverter.SQLA_TYPE_MAPPING.copy() + SQLA_TYPE_MAPPING.update({ + UTCDateTime: fields.DateTime, +- PyIntEnum: EnumField ++ PyIntEnum: fields.Enum + }) + + def _get_field_kwargs_for_property(self, prop): +diff --git a/indico/modules/categories/controllers/display.py b/indico/modules/categories/controllers/display.py +index 00d229c..b2e0707 100644 +--- a/indico/modules/categories/controllers/display.py ++++ b/indico/modules/categories/controllers/display.py +@@ -17,7 +17,6 @@ import dateutil + from dateutil.parser import ParserError + from dateutil.relativedelta import relativedelta + from flask import flash, jsonify, redirect, request, session +-from marshmallow_enum import EnumField + from pytz import utc + from sqlalchemy.orm import joinedload, load_only, selectinload, subqueryload, undefer, undefer_group + from webargs import fields, validate +@@ -575,7 +574,7 @@ class RHCategoryCalendarViewEvents(RHDisplayCategoryBase): + room = auto() + keywords = auto() + +- @use_kwargs({'group_by': EnumField(GroupBy, load_default=GroupBy.category)}, location='query') ++ @use_kwargs({'group_by': fields.Enum(GroupBy, load_default=GroupBy.category)}, location='query') + def _process_args(self, group_by): + RHDisplayCategoryBase._process_args(self) + tz = self.category.display_tzinfo +diff --git a/indico/modules/events/contributions/schemas.py b/indico/modules/events/contributions/schemas.py +index 3ea9d17..0f1df57 100644 +--- a/indico/modules/events/contributions/schemas.py ++++ b/indico/modules/events/contributions/schemas.py +@@ -9,7 +9,6 @@ import hashlib + from operator import attrgetter + + from marshmallow import fields, post_dump +-from marshmallow_enum import EnumField + from marshmallow_sqlalchemy import column2field + + from indico.core.marshmallow import mm +@@ -38,7 +37,7 @@ class ContributionTypeSchema(mm.SQLAlchemyAutoSchema): + + + class ContributionFieldSchema(mm.Schema): +- visibility = EnumField(ContributionFieldVisibility) ++ visibility = fields.Enum(ContributionFieldVisibility) + + class Meta: + model = ContributionField +diff --git a/indico/modules/events/controllers/display.py b/indico/modules/events/controllers/display.py +index e7d4830..eb4aa57 100644 +--- a/indico/modules/events/controllers/display.py ++++ b/indico/modules/events/controllers/display.py +@@ -8,7 +8,6 @@ + from io import BytesIO + + from flask import jsonify, redirect, request, session +-from marshmallow_enum import EnumField + from webargs import fields + + from indico.modules.events.controllers.base import RHDisplayEventBase, RHEventBase +@@ -27,7 +26,7 @@ from indico.web.rh import RHProtected, allow_signed_url + @allow_signed_url + class RHExportEventICAL(RHDisplayEventBase): + @use_kwargs({ +- 'scope': EnumField(CalendarScope, load_default=None), ++ 'scope': fields.Enum(CalendarScope, load_default=None), + 'detail': fields.String(load_default=None), + 'series': fields.Boolean(load_default=False) # Export the full event series + }, location='query') +diff --git a/indico/modules/events/editing/controllers/backend/timeline.py b/indico/modules/events/editing/controllers/backend/timeline.py +index 1088178..4c9a807 100644 +--- a/indico/modules/events/editing/controllers/backend/timeline.py ++++ b/indico/modules/events/editing/controllers/backend/timeline.py +@@ -13,7 +13,6 @@ from zipfile import ZipFile, ZipInfo + + from flask import jsonify, request, session + from marshmallow import EXCLUDE, fields +-from marshmallow_enum import EnumField + from sqlalchemy.orm import joinedload + from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, ServiceUnavailable + +@@ -248,7 +247,7 @@ class RHConfirmEditableChanges(RHContributionEditableRevisionBase): + return self.editable.can_perform_submitter_actions(session.user) + + @use_kwargs({ +- 'action': EnumField(EditingConfirmationAction, required=True), ++ 'action': fields.Enum(EditingConfirmationAction, required=True), + 'comment': fields.String(load_default='') + }) + def _process(self, action, comment): +@@ -267,7 +266,7 @@ class RHReplaceRevision(RHContributionEditableRevisionBase): + + @use_kwargs({ + 'comment': fields.String(load_default=''), +- 'revision_type': EnumField(RevisionType) ++ 'revision_type': fields.Enum(RevisionType) + }) + def _process(self, comment, revision_type): + args = parser.parse({ +diff --git a/indico/modules/events/editing/schemas.py b/indico/modules/events/editing/schemas.py +index f3c022e..b5da577 100644 +--- a/indico/modules/events/editing/schemas.py ++++ b/indico/modules/events/editing/schemas.py +@@ -10,7 +10,6 @@ from operator import itemgetter + + from markupsafe import escape + from marshmallow import ValidationError, fields, post_dump, validate, validates, validates_schema +-from marshmallow_enum import EnumField + from sqlalchemy import func + + from indico.core.marshmallow import mm +@@ -251,7 +250,7 @@ class EditableBasicSchema(mm.SQLAlchemyAutoSchema): + model = Editable + fields = ('id', 'type', 'state', 'editor', 'timeline_url', 'revision_count', 'tags', 'last_update_dt') + +- state = EnumField(EditableState) ++ state = fields.Enum(EditableState) + editor = fields.Nested(EditingUserSchema) + timeline_url = fields.String() + tags = fields.List(fields.Nested(EditingTagSchema, only=('id', 'code', 'title', 'color')), +@@ -295,7 +294,7 @@ class FilteredEditableSchema(mm.SQLAlchemyAutoSchema): + attribute='contribution.person_links') + contribution_keywords = fields.List(fields.String(), attribute='contribution.keywords') + contribution_session = fields.Nested(SessionBasicSchema, attribute='contribution.session') +- state = EnumField(EditableState) ++ state = fields.Enum(EditableState) + timeline_url = fields.String() + editor = fields.Nested(EditingUserSchema) + can_assign_self = fields.Function(lambda editable, ctx: editable.can_assign_self(ctx.get('user'))) +@@ -318,7 +317,7 @@ class EditingReviewAction(IndicoEnum): + + + class ReviewEditableArgs(mm.Schema): +- action = EnumField(EditingReviewAction, required=True) ++ action = fields.Enum(EditingReviewAction, required=True) + comment = fields.String(load_default='') + + @validates_schema(skip_on_field_errors=True) +@@ -437,7 +436,7 @@ class EditingMenuItemSchema(mm.Schema): + + + class EditableTypeArgs(mm.Schema): +- editable_types = fields.List(EnumField(EditableType), required=True) ++ editable_types = fields.List(fields.Enum(EditableType), required=True) + + + class EditableTypePrincipalsSchema(mm.Schema): +diff --git a/indico/modules/events/notes/controllers.py b/indico/modules/events/notes/controllers.py +index 4e8a17e..c5b3191 100644 +--- a/indico/modules/events/notes/controllers.py ++++ b/indico/modules/events/notes/controllers.py +@@ -6,7 +6,6 @@ + # LICENSE file for more details. + + from flask import jsonify, redirect, request, session +-from marshmallow_enum import EnumField + from webargs import fields + from werkzeug.exceptions import Forbidden, NotFound + +@@ -70,7 +69,7 @@ class RHApiNote(RHManageNoteBase): + return EventNoteSchema().dump(note.current_revision) + + @use_kwargs({ +- 'render_mode': EnumField(RenderMode, load_default=RenderMode.html), ++ 'render_mode': fields.Enum(RenderMode, load_default=RenderMode.html), + 'source': fields.String(required=True), + 'revision_id': fields.Integer(), + }) +diff --git a/indico/modules/events/papers/controllers/api.py b/indico/modules/events/papers/controllers/api.py +index c06042c..183bc7f 100644 +--- a/indico/modules/events/papers/controllers/api.py ++++ b/indico/modules/events/papers/controllers/api.py +@@ -6,7 +6,6 @@ + # LICENSE file for more details. + + from flask import request, session +-from marshmallow_enum import EnumField + from webargs import fields, validate + from werkzeug.exceptions import Forbidden, UnprocessableEntity + +@@ -50,7 +49,7 @@ class RHCreatePaperComment(RHPaperBase): + + @use_kwargs({ + 'comment': fields.String(validate=not_empty), +- 'visibility': EnumField(PaperCommentVisibility, load_default=None) ++ 'visibility': fields.Enum(PaperCommentVisibility, load_default=None) + }) + def _process(self, comment, visibility): + create_comment(self.paper, comment, visibility, session.user) +@@ -82,7 +81,7 @@ class RHCommentActions(RHPaperBase): + + @use_kwargs({ + 'comment': fields.String(validate=not_empty), +- 'visibility': EnumField(PaperCommentVisibility) ++ 'visibility': fields.Enum(PaperCommentVisibility) + }, partial=True) + def _process_PATCH(self, comment=None, visibility=None): + update_comment(self.comment, comment, visibility) +@@ -94,7 +93,7 @@ class RHJudgePaper(RHPaperBase): + return self.paper.can_judge(session.user, check_state=True) + + @use_kwargs({ +- 'action': EnumField(PaperAction, required=True), ++ 'action': fields.Enum(PaperAction, required=True), + 'comment': fields.String() + }) + def _process(self, action, comment): +@@ -122,7 +121,7 @@ class RHSubmitNewRevision(RHPaperBase): + + def _parse_review_args(event, review_type): + args_schema = { +- 'proposed_action': EnumField(PaperAction, required=True), ++ 'proposed_action': fields.Enum(PaperAction, required=True), + 'comment': fields.String(load_default='') + } + +diff --git a/indico/modules/events/papers/schemas.py b/indico/modules/events/papers/schemas.py +index 4b228ab..fd47bcf 100644 +--- a/indico/modules/events/papers/schemas.py ++++ b/indico/modules/events/papers/schemas.py +@@ -7,8 +7,7 @@ + + from markupsafe import escape + from marshmallow import post_dump +-from marshmallow.fields import Boolean, Decimal, Field, Function, Integer, List, Method, Nested, String +-from marshmallow_enum import EnumField ++from marshmallow.fields import Boolean, Decimal, Enum, Field, Function, Integer, List, Method, Nested, String + + from indico.core.marshmallow import mm + from indico.modules.events.contributions.schemas import BasicContributionSchema +@@ -100,7 +99,7 @@ class PaperRevisionSchema(mm.SQLAlchemyAutoSchema): + judge = Nested(BasicUserSchema) + spotlight_file = Nested(PaperFileSchema) + files = List(Nested(PaperFileSchema)) +- state = EnumField(PaperRevisionState) ++ state = Enum(PaperRevisionState) + timeline = PaperRevisionTimelineField() + judgment_comment_html = Function(lambda revision: escape(revision.judgment_comment)) + reviewer_data = Method('_get_reviewer_data') +diff --git a/indico/modules/events/persons/schemas.py b/indico/modules/events/persons/schemas.py +index 9de1947..f122ae1 100644 +--- a/indico/modules/events/persons/schemas.py ++++ b/indico/modules/events/persons/schemas.py +@@ -6,7 +6,6 @@ + # LICENSE file for more details. + + from marshmallow import fields, post_dump, post_load, pre_load +-from marshmallow_enum import EnumField + + from indico.core.marshmallow import mm + from indico.modules.events.models.persons import EventPerson +@@ -24,7 +23,7 @@ class PersonLinkSchema(mm.Schema): + name = fields.String(attribute='display_full_name', dump_only=True) + first_name = fields.String(load_default='') + last_name = fields.String(required=True) +- _title = EnumField(UserTitle, data_key='title') ++ _title = fields.Enum(UserTitle, data_key='title') + affiliation = fields.String(load_default='') + affiliation_link = ModelField(Affiliation, data_key='affiliation_id', load_default=None, load_only=True) + affiliation_id = fields.Integer(load_default=None, dump_only=True) +@@ -100,4 +99,4 @@ class EventPersonUpdateSchema(EventPersonSchema): + class Meta(EventPersonSchema.Meta): + fields = ('title', 'first_name', 'last_name', 'address', 'phone', 'affiliation', 'affiliation_link') + +- title = EnumField(UserTitle) ++ title = fields.Enum(UserTitle) +diff --git a/indico/modules/events/registration/controllers/management/privacy.py b/indico/modules/events/registration/controllers/management/privacy.py +index eabbe3b..4bbd6f0 100644 +--- a/indico/modules/events/registration/controllers/management/privacy.py ++++ b/indico/modules/events/registration/controllers/management/privacy.py +@@ -7,7 +7,7 @@ + + from flask import redirect, session + from flask.helpers import flash +-from marshmallow_enum import EnumField ++from marshmallow import fields + + from indico.core.db import db + from indico.modules.events import EventLogRealm +@@ -64,7 +64,7 @@ class RHRegistrationPrivacy(RHManageRegFormBase): + class RHAPIRegistrationChangeConsent(RHRegistrationFormRegistrationBase): + """Internal API to change registration consent to publish.""" + +- @use_kwargs({'consent_to_publish': EnumField(RegistrationVisibility)}) ++ @use_kwargs({'consent_to_publish': fields.Enum(RegistrationVisibility)}) + def _process_POST(self, consent_to_publish): + update_registration_consent_to_publish(self.registration, consent_to_publish) + return '', 204 +diff --git a/indico/modules/events/registration/util.py b/indico/modules/events/registration/util.py +index c370676..ea03684 100644 +--- a/indico/modules/events/registration/util.py ++++ b/indico/modules/events/registration/util.py +@@ -15,7 +15,6 @@ from operator import attrgetter + + from flask import json, session + from marshmallow import RAISE, ValidationError, fields, validates +-from marshmallow_enum import EnumField + from PIL import Image, ImageOps + from qrcode import QRCode, constants + from sqlalchemy import and_, or_ +@@ -349,7 +348,7 @@ def make_registration_schema( + schema['notify_user'] = fields.Boolean() + schema['override_required'] = fields.Boolean() + elif regform.needs_publish_consent: +- schema['consent_to_publish'] = EnumField(RegistrationVisibility) ++ schema['consent_to_publish'] = fields.Enum(RegistrationVisibility) + + if captcha_required: + schema['captcha'] = CaptchaField() +diff --git a/indico/modules/rb/controllers/backend/bookings.py b/indico/modules/rb/controllers/backend/bookings.py +index 30f95dd..c361b68 100644 +--- a/indico/modules/rb/controllers/backend/bookings.py ++++ b/indico/modules/rb/controllers/backend/bookings.py +@@ -11,7 +11,6 @@ from datetime import date, datetime, time + import dateutil + from flask import jsonify, request, session + from marshmallow import fields, validate +-from marshmallow_enum import EnumField + from sqlalchemy.orm import joinedload + from werkzeug.exceptions import BadRequest, Forbidden, NotFound + +@@ -65,7 +64,7 @@ class RHTimeline(RHRoomBookingBase): + @use_kwargs({ + 'start_dt': fields.DateTime(required=True), + 'end_dt': fields.DateTime(required=True), +- 'repeat_frequency': EnumField(RepeatFrequency, load_default='NEVER'), ++ 'repeat_frequency': fields.Enum(RepeatFrequency, load_default='NEVER'), + 'repeat_interval': fields.Int(load_default=1), + 'recurrence_weekdays': fields.List(fields.Str(validate=validate.OneOf(WEEKDAYS)), load_default=None), + 'skip_conflicts_with': fields.List(fields.Int(), load_default=None), +@@ -311,7 +310,7 @@ class RHBookingEditCalendars(RHBookingBase): + @use_kwargs({ + 'start_dt': fields.DateTime(required=True), + 'end_dt': fields.DateTime(required=True), +- 'repeat_frequency': EnumField(RepeatFrequency, load_default='NEVER'), ++ 'repeat_frequency': fields.Enum(RepeatFrequency, load_default='NEVER'), + 'repeat_interval': fields.Int(load_default=1), + 'recurrence_weekdays': fields.List(fields.Str(validate=validate.OneOf(WEEKDAYS)), load_default=None) + }, location='query') +@@ -395,7 +394,7 @@ class RHMatchingEvents(RHRoomBookingBase): + @use_kwargs({ + 'start_dt': fields.DateTime(), + 'end_dt': fields.DateTime(), +- 'repeat_frequency': EnumField(RepeatFrequency, load_default='NEVER'), ++ 'repeat_frequency': fields.Enum(RepeatFrequency, load_default='NEVER'), + 'repeat_interval': fields.Int(load_default=1), + 'recurrence_weekdays': fields.List(fields.Str(validate=validate.OneOf(WEEKDAYS)), load_default=None) + }, location='query') +diff --git a/indico/modules/rb/controllers/backend/common.py b/indico/modules/rb/controllers/backend/common.py +index 5387c4f..38ecc9e 100644 +--- a/indico/modules/rb/controllers/backend/common.py ++++ b/indico/modules/rb/controllers/backend/common.py +@@ -6,7 +6,6 @@ + # LICENSE file for more details. + + from marshmallow import validate +-from marshmallow_enum import EnumField + from webargs import fields + + from indico.modules.rb.models.reservations import RepeatFrequency +@@ -23,7 +22,7 @@ search_room_args = { + 'division': fields.Str(), + 'start_dt': fields.DateTime(), + 'end_dt': fields.DateTime(), +- 'repeat_frequency': EnumField(RepeatFrequency), ++ 'repeat_frequency': fields.Enum(RepeatFrequency), + 'repeat_interval': fields.Int(load_default=0), + 'recurrence_weekdays': fields.List(fields.Str(validate=validate.OneOf(WEEKDAYS))), + 'building': fields.Str(), +diff --git a/indico/modules/rb/schemas.py b/indico/modules/rb/schemas.py +index d9cafb8..1bc9854 100644 +--- a/indico/modules/rb/schemas.py ++++ b/indico/modules/rb/schemas.py +@@ -11,7 +11,6 @@ from babel.dates import get_timezone + from flask import session + from marshmallow import ValidationError, fields, post_dump, post_load, validate, validates, validates_schema + from marshmallow.fields import Boolean, Date, DateTime, Function, Method, Nested, Number, Pluck, String +-from marshmallow_enum import EnumField + from sqlalchemy import func + + from indico.core.config import config +@@ -79,7 +78,7 @@ class AdminRoomSchema(mm.SQLAlchemyAutoSchema): + class RoomUpdateSchema(RoomSchema): + owner = Principal() + acl_entries = PrincipalPermissionList(RoomPrincipal) +- protection_mode = EnumField(ProtectionMode) ++ protection_mode = fields.Enum(ProtectionMode) + + class Meta(RoomSchema.Meta): + fields = (*RoomSchema.Meta.fields, 'notification_before_days', 'notification_before_days_weekly', 'owner', +@@ -117,7 +116,7 @@ class RoomUpdateArgsSchema(mm.Schema): + max_advance_days = fields.Int(validate=lambda x: x >= 1, allow_none=True) + comments = fields.String() + acl_entries = PrincipalPermissionList(RoomPrincipal) +- protection_mode = EnumField(ProtectionMode) ++ protection_mode = fields.Enum(ProtectionMode) + + + class RoomEquipmentSchema(mm.SQLAlchemyAutoSchema): +@@ -185,11 +184,11 @@ class ReservationUserEventSchema(mm.Schema): + + class ReservationOccurrenceLinkSchema(mm.SQLAlchemyAutoSchema): + id = Number() +- type = EnumField(LinkType, attribute='link_type') ++ type = fields.Enum(LinkType, attribute='link_type') + object = Nested(ReservationLinkedObjectDataSchema, + only=('url', 'title', 'event_title', 'event_url', 'start_dt', 'end_dt')) + start_dt = NaiveDateTime(attribute='reservation_occurrence.start_dt') +- state = EnumField(ReservationOccurrenceState, attribute='reservation_occurrence.state') ++ state = fields.Enum(ReservationOccurrenceState, attribute='reservation_occurrence.state') + + @post_dump(pass_original=True) + def _hide_restricted_object(self, data, link, **kwargs): +@@ -204,7 +203,7 @@ class ReservationOccurrenceLinkSchema(mm.SQLAlchemyAutoSchema): + + class ReservationOccurrenceSchema(mm.SQLAlchemyAutoSchema): + reservation = Nested(ReservationSchema) +- state = EnumField(ReservationOccurrenceState) ++ state = fields.Enum(ReservationOccurrenceState) + start_dt = NaiveDateTime() + end_dt = NaiveDateTime() + +@@ -262,7 +261,7 @@ class ReservationDetailsSchema(mm.SQLAlchemyAutoSchema): + can_edit = Function(lambda booking: booking.can_edit(session.user)) + can_reject = Function(lambda booking: booking.can_reject(session.user)) + permissions = Method('_get_permissions') +- state = EnumField(ReservationState) ++ state = fields.Enum(ReservationState) + is_linked_to_objects = Function(lambda booking: bool(booking.links)) + start_dt = NaiveDateTime() + end_dt = NaiveDateTime() +@@ -302,7 +301,7 @@ class ReservationDetailsSchema(mm.SQLAlchemyAutoSchema): + + class BlockedRoomSchema(mm.SQLAlchemyAutoSchema): + room = Nested(RoomSchema, only=('id', 'name', 'sprite_position', 'full_name')) +- state = EnumField(BlockedRoomState) ++ state = fields.Enum(BlockedRoomState) + + class Meta: + model = BlockedRoom +@@ -398,7 +397,7 @@ class CreateBookingSchema(mm.Schema): + + start_dt = fields.DateTime(required=True) + end_dt = fields.DateTime(required=True) +- repeat_frequency = EnumField(RepeatFrequency, required=True) ++ repeat_frequency = fields.Enum(RepeatFrequency, required=True) + repeat_interval = fields.Int(load_default=0, validate=lambda x: x >= 0) + recurrence_weekdays = fields.List(fields.Str(validate=validate.OneOf(WEEKDAYS))) + room_id = fields.Int(required=True) +@@ -406,7 +405,7 @@ class CreateBookingSchema(mm.Schema): + booking_reason = fields.String(data_key='reason', load_default='') + internal_note = fields.String() + is_prebooking = fields.Bool(load_default=False) +- link_type = EnumField(LinkType) ++ link_type = fields.Enum(LinkType) + link_id = fields.Int() + link_back = fields.Bool(load_default=False) + admin_override_enabled = fields.Bool(load_default=False) +@@ -561,7 +560,7 @@ class SettingsSchema(mm.Schema): + hide_module_if_unauthorized = fields.Bool() + tileserver_url = fields.String(validate=validate.URL(schemes={'http', 'https'}), allow_none=True) + booking_limit = fields.Int(validate=not_empty) +- booking_reason_required = EnumField(BookingReasonRequiredOptions, required=True) ++ booking_reason_required = fields.Enum(BookingReasonRequiredOptions, required=True) + notifications_enabled = fields.Bool() + notification_before_days = fields.Int(validate=validate.Range(min=1, max=30)) + notification_before_days_weekly = fields.Int(validate=validate.Range(min=1, max=30)) +diff --git a/indico/modules/search/controllers.py b/indico/modules/search/controllers.py +index a115a81..7f818d9 100644 +--- a/indico/modules/search/controllers.py ++++ b/indico/modules/search/controllers.py +@@ -7,7 +7,6 @@ + + from flask import jsonify, session + from marshmallow import INCLUDE, fields +-from marshmallow_enum import EnumField + + from indico.modules.categories.controllers.base import RHDisplayCategoryBase + from indico.modules.events.controllers.base import RHDisplayEventBase +@@ -46,7 +45,7 @@ class RHAPISearch(RH): + @use_kwargs({ + 'page': fields.Int(load_default=None), + 'q': fields.String(required=True), +- 'type': fields.List(EnumField(SearchTarget), required=True), ++ 'type': fields.List(fields.Enum(SearchTarget), required=True), + 'admin_override_enabled': fields.Bool( + load_default=False, + validate=validate_with_message(lambda value: session.user and session.user.is_admin, +diff --git a/indico/modules/search/result_schemas.py b/indico/modules/search/result_schemas.py +index 88c274e..96efa43 100644 +--- a/indico/modules/search/result_schemas.py ++++ b/indico/modules/search/result_schemas.py +@@ -6,7 +6,6 @@ + # LICENSE file for more details. + + from marshmallow import EXCLUDE, ValidationError, fields +-from marshmallow_enum import EnumField + from marshmallow_oneofschema import OneOfSchema + + from indico.core.db.sqlalchemy.links import LinkType +@@ -63,7 +62,7 @@ def require_search_target(target): + + + class CategoryResultSchema(ResultSchemaBase): +- type = EnumField(SearchTarget, validate=require_search_target(SearchTarget.category)) ++ type = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.category)) + category_id = fields.Int(required=True) + title = fields.String(required=True) + url = fields.Method('_get_url') +@@ -83,7 +82,7 @@ class LocationResultSchema(mm.Schema): + + class EventResultSchema(ResultSchemaBase): + #: The record type +- type: SearchTarget = EnumField(SearchTarget, validate=require_search_target(SearchTarget.event)) ++ type: SearchTarget = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.event)) + #: The event id + event_id = fields.Int(required=True) + #: The event title +@@ -91,7 +90,7 @@ class EventResultSchema(ResultSchemaBase): + #: The event description + description = fields.String(required=True) + #: The event type +- event_type = EnumField(EventType, required=True) ++ event_type = fields.Enum(EventType, required=True) + #: The event start date time + start_dt = fields.DateTime(required=True) + #: The event end date time +@@ -111,7 +110,7 @@ class EventResultSchema(ResultSchemaBase): + + class ContributionResultSchema(ResultSchemaBase): + #: The record type +- type: SearchTarget = EnumField(SearchTarget, validate=require_search_target(SearchTarget.contribution)) ++ type: SearchTarget = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.contribution)) + #: The contribution id + contribution_id = fields.Int(required=True) + #: The contribution event id +@@ -150,7 +149,7 @@ class ContributionResultSchema(ResultSchemaBase): + + class SubContributionResultSchema(ContributionResultSchema): + #: The record type +- type: SearchTarget = EnumField(SearchTarget, validate=require_search_target(SearchTarget.subcontribution)) ++ type: SearchTarget = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.subcontribution)) + #: The sub-contribution id + subcontribution_id = fields.Int(required=True) + +@@ -188,7 +187,7 @@ def _get_event_path(obj): + + class AttachmentResultSchema(ResultSchemaBase): + #: The record type +- type: SearchTarget = EnumField(SearchTarget, validate=require_search_target(SearchTarget.attachment)) ++ type: SearchTarget = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.attachment)) + #: The attachment id + attachment_id = fields.Int(required=True) + #: The attachment folder id +@@ -206,7 +205,7 @@ class AttachmentResultSchema(ResultSchemaBase): + #: The attachment author + user: PersonSchema = fields.Nested(PersonSchema, load_default=None) + #: The attachment type +- attachment_type: AttachmentType = EnumField(AttachmentType, required=True) ++ attachment_type: AttachmentType = fields.Enum(AttachmentType, required=True) + #: The attachment last modified date time + modified_dt = fields.DateTime(required=True) + # extra fields that are not taken from the data returned by the search engine +@@ -227,7 +226,7 @@ class AttachmentResultSchema(ResultSchemaBase): + + class EventNoteResultSchema(ResultSchemaBase): + #: The record type +- type: SearchTarget = EnumField(SearchTarget, validate=require_search_target(SearchTarget.event_note)) ++ type: SearchTarget = fields.Enum(SearchTarget, validate=require_search_target(SearchTarget.event_note)) + #: The note id + note_id = fields.Int(required=True) + #: The note event id +diff --git a/indico/modules/users/controllers.py b/indico/modules/users/controllers.py +index 5cb5047..52c7a51 100644 +--- a/indico/modules/users/controllers.py ++++ b/indico/modules/users/controllers.py +@@ -15,7 +15,6 @@ from flask import flash, jsonify, redirect, render_template, request, session + from itsdangerous import BadSignature + from markupsafe import Markup, escape + from marshmallow import fields +-from marshmallow_enum import EnumField + from PIL import Image + from sqlalchemy.orm import joinedload, load_only, subqueryload + from sqlalchemy.orm.exc import StaleDataError +@@ -337,7 +336,7 @@ class RHProfilePicturePreview(RHUserBase): + + flash_user_status = False + +- @use_kwargs({'source': EnumField(ProfilePictureSource)}, location='view_args') ++ @use_kwargs({'source': fields.Enum(ProfilePictureSource)}, location='view_args') + def _process(self, source): + if source == ProfilePictureSource.standard: + first_name = self.user.first_name[0].upper() if self.user.first_name else '' +@@ -374,7 +373,7 @@ class RHSaveProfilePicture(RHUserBase): + """Update the user's profile picture.""" + + @use_kwargs({ +- 'source': EnumField(ProfilePictureSource, required=True) ++ 'source': fields.Enum(ProfilePictureSource, required=True) + }) + def _process(self, source): + self.user.picture_source = source +@@ -885,7 +884,7 @@ class RHRejectRegistrationRequest(RHRegistrationRequestBase): + class UserSearchResultSchema(mm.SQLAlchemyAutoSchema): + affiliation_id = fields.Integer(attribute='_affiliation.affiliation_id') + affiliation_meta = fields.Nested(AffiliationSchema, attribute='_affiliation.affiliation_link') +- title = EnumField(UserTitle, attribute='_title') ++ title = fields.Enum(UserTitle, attribute='_title') + + class Meta: + model = User +diff --git a/indico/util/marshmallow.py b/indico/util/marshmallow.py +index fed7293..b331b4a 100644 +--- a/indico/util/marshmallow.py ++++ b/indico/util/marshmallow.py +@@ -14,7 +14,6 @@ import yaml + from dateutil import parser, relativedelta + from marshmallow import Schema, ValidationError, fields + from marshmallow.utils import from_iso_datetime +-from marshmallow_enum import EnumField + from pytz import timezone + from speaklater import _LazyString + from sqlalchemy import inspect +@@ -500,7 +499,7 @@ class LowercaseString(fields.String): + return super()._deserialize(value, attr, data, **kwargs).lower() + + +-class NoneValueEnumField(EnumField): ++class NoneValueEnumField(fields.Enum): + """ + Like the normal EnumField, but when receiving a None value, + this is mapped to a specific enum member. +diff --git a/requirements.in b/requirements.in +index 54be3ee..fd12cfa 100644 +--- a/requirements.in ++++ b/requirements.in +@@ -36,7 +36,6 @@ lxml[html5] + markdown + markupsafe + marshmallow-dataclass +-marshmallow-enum + marshmallow-oneofschema + marshmallow-sqlalchemy + marshmallow +diff --git a/requirements.txt b/requirements.txt +index e1112a7814..53c2d521de 100644 +--- a/requirements.txt ++++ b/requirements.txt +@@ -211,14 +211,11 @@ marshmallow==3.22.0 + # -r requirements.in + # flask-marshmallow + # marshmallow-dataclass +- # marshmallow-enum + # marshmallow-oneofschema + # marshmallow-sqlalchemy + # webargs + marshmallow-dataclass==8.7.0 + # via -r requirements.in +-marshmallow-enum==1.5.1 +- # via -r requirements.in + marshmallow-oneofschema==3.1.1 + # via -r requirements.in + marshmallow-sqlalchemy==1.1.0 \ No newline at end of file diff --git a/pkgs/pynpm/default.nix b/pkgs/pynpm/default.nix new file mode 100644 index 0000000..df945a0 --- /dev/null +++ b/pkgs/pynpm/default.nix @@ -0,0 +1,34 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + wheel, + babel, +}: + +buildPythonPackage rec { + pname = "pynpm"; + version = "0.2.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "inveniosoftware"; + repo = "pynpm"; + rev = "refs/tags/v${version}"; + hash = "sha256-GjfylyxPZrRGCLuNZejmTdQGz5JugKHYpVix8WF/8QK="; + }; + + build-system = [ + setuptools + wheel + babel + ]; + + meta = { + description = "Python interface to your NPM and package.json"; + homepage = "https://github.com/inveniosoftware/pynpm"; + changelog = "https://github.com/inveniosoftware/pynpm/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/pywebpack/default.nix b/pkgs/pywebpack/default.nix new file mode 100644 index 0000000..b4b2039 --- /dev/null +++ b/pkgs/pywebpack/default.nix @@ -0,0 +1,41 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + wheel, + babel, + importlib-metadata, + pynpm, +}: + +buildPythonPackage rec { + pname = "pywebpack"; + version = "2.1.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "inveniosoftware"; + repo = "pywebpack"; + rev = "refs/tags/v${version}"; + hash = "sha256-6hK/k+26Iqb6BE/pv2iFFg5OH6247AAGpvOV6dDRKRT="; + }; + + build-system = [ + setuptools + wheel + babel + ]; + + dependencies = [ + importlib-metadata + pynpm + ]; + + meta = { + description = "Webpack integration layer for Python"; + homepage = "https://github.com/inveniosoftware/pywebpack"; + changelog = "https://github.com/inveniosoftware/pywebpack/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/wtforms-dateutil/default.nix b/pkgs/wtforms-dateutil/default.nix new file mode 100644 index 0000000..1e50f4c --- /dev/null +++ b/pkgs/wtforms-dateutil/default.nix @@ -0,0 +1,36 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + wtforms, + python-dateutil, +}: + +buildPythonPackage rec { + pname = "wtforms-dateutil"; + version = "0.1.0"; + pyproject = true; + + src = fetchFromGitHub { + owner = "wtforms"; + repo = "wtforms-dateutil"; + rev = "refs/tags/${version}"; + hash = "sha256-k1DZqf2FgyupNvRIIrPLwxbTP5MteCnvASCkpCZRyAE="; + }; + + build-system = [ setuptools ]; + + dependencies = [ + wtforms + python-dateutil + ]; + + pythonImportChecks = [ "wtforms_dateutil" ]; + + meta = { + description = "WTForms fields using dateutil"; + homepage = "https://github.com/pallets-eco/wtforms-dateutil"; + license = lib.licenses.bsd3; + }; +} diff --git a/pkgs/wtforms-sqlalchemy/default.nix b/pkgs/wtforms-sqlalchemy/default.nix new file mode 100644 index 0000000..bf592d1 --- /dev/null +++ b/pkgs/wtforms-sqlalchemy/default.nix @@ -0,0 +1,37 @@ +{ + lib, + fetchFromGitHub, + buildPythonPackage, + setuptools, + sqlalchemy, + wtforms, +}: + +buildPythonPackage rec { + pname = "wtforms-sqlalchemy"; + version = "0.4.1"; + pyproject = true; + + src = fetchFromGitHub { + owner = "wtforms"; + repo = "wtforms-sqlalchemy"; + rev = "refs/tags/${version}"; + hash = "sha256-uR09LYfcyre+AC2TTEIhpjSI7y4Yo0GJ20smkzo5PRY="; + }; + + build-system = [ setuptools ]; + + dependencies = [ + sqlalchemy + wtforms + ]; + + pythonImportsCheck = [ "wtforms_sqlalchemy" ]; + + meta = { + description = "WTForms integration for SQLAlchemy"; + homepage = "https://github.com/wtforms/wtforms-sqlalchemy"; + changelog = "https://github.com/wtforms/wtforms-sqlalchemy/blob/${version}/CHANGES.rst"; + license = lib.licenses.bsd3; + }; +} diff --git a/profiles/indico.nix b/profiles/indico.nix new file mode 100644 index 0000000..8f6350e --- /dev/null +++ b/profiles/indico.nix @@ -0,0 +1,116 @@ +{ config, lib, pkgs, python3, ... }: +let + # cfg = config.services.indico; + # pythonFmt = pkgs.formats.pythonVars { }; + indico = pkgs.callPackage ../pkgs/indico { }; + pythonEnv = indico.python.withPackages (ps: [ + indico + # (ps.toPythonModule indico) + ps.gunicorn + ]); +in +{ + # TODO cProfile; indico is *very* slow (~30s just to print the help) + + # + sudo indico + environment.systemPackages = [ indico ]; + + services.redis.servers.indico.enable = true; + + systemd.services.indico-web = { + description = "Indico web service"; + after = [ + "network.target" + "redis-indico.service" + "postgresql.service" + ]; + wantedBy = [ "multi-user.target" ]; + # TODO migrations + serviceConfig = { + User = "indico"; + Group = "indico"; + ExecStart = "${lib.getExe' pythonEnv "gunicorn"} --bind unix:/run/indico/indico.sock --name=indico indico.wsgi"; + Restart = "on-failure"; + }; + enable = true; + }; + + systemd.sockets.gunicorn-web = { + socketConfig = { + ListenStream = "/run/indico/indico.sock"; + SocketUser = "nginx"; + }; + enable = true; + }; + # preStart = '' + # echo "create extension if not exists pg_trgm" | runuser -u ${config.services.postgresql.superUser} -- ${config.services.postgresql.package}/bin/psql hydra + # '' + + services.postgresql = { + enable = true; + package = pkgs.postgresql_16; + ensureDatabases = [ "indico" ]; + ensureUsers = [ + { + name = "indico"; + ensureDBOwnership = true; + } + ]; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + + services.nginx = let + indicoBaseDir = "/tmp"; + in { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + upstreams.indico.servers."unix:/run/indico/indico.sock" = { }; + virtualHosts."events.federez.net" = { + enableACME = true; + forceSSL = true; + locations = { + "/.xsf/indico/" = { + alias = "${indicoBaseDir}/"; + extraConfig = '' + internal; + ''; + }; + # Order? + too lax? + "~ ^/(images|fonts)(.*)/(.+?)(__v[0-9a-f]+)?\\.([^.]+)$" = { + alias = "${indicoBaseDir}/web/static/$1$2/$3.$5"; + extraConfig = '' + access_log off; + ''; + }; + "~ ^/(css|dist|images|fonts)/(.*)$" = { + alias = "${indicoBaseDir}/web/static/$1/$2"; + extraConfig = '' + access_log off; + ''; + }; + "= /robots.txt" = { + alias = "${indicoBaseDir}/web/static/robots.txt"; + extraConfig = '' + access_log off; + ''; + "/" = { + proxyPass = "http://indico"; + extraConfig = '' + client_max_body_size 1G; + ''; + }; + }; + }; + }; + + users.users.indico = { + isSystemUser = true; + group = "indico"; + }; + + users.groups.indico = {}; +} diff --git a/secrets/discourse-key-base.age b/secrets/discourse-key-base.age index ff836d1..04928f7 100644 --- a/secrets/discourse-key-base.age +++ b/secrets/discourse-key-base.age @@ -1,33 +1,33 @@ age-encryption.org/v1 --> ssh-ed25519 oDAQrw NFtzIriHmvcPmluvbL9yV5fAFKoBWcuY4M3Qy1PogkU -KxRzRwZhrvVxvvRkuVsumYRbUDXsYecvmvKR8osv4/s +-> ssh-ed25519 oDAQrw SBbVb1mkK/vYXtTNGbb7l0sNuusSObbrp6ft41xUHCo +7BokqAWzTauGghPtytuJ/GN1qD+1kZAHGbiKrb4yc2U -> ssh-rsa krWCLQ -e6/eduQLNkjdwInZNonT6xtrRAtlt8UJqrTvUajsfQzR+AcFfcMGuOdzwRIAdbMq -lC8mICFn+9RwwA1360CFSGSZOBrhWnY5Ov9KPsXGOYiup4/fDwk6NJxurnTSJVrb -EUyBQoeds9ZnHlQ9Jq5tjnzVo0gIIm1ixtOq9XnAos4tiDzviZgjimJRZL7zW7Zd -4FQBwXSn8rTT7/gBM4oDbDmBcrbAf9YcP9Wspsye1XgTG/KyqRT75VAJu2l7fUnU -DIlqmQbPPkSIO4MURPXpAg8S3tXAC6fs69s77dq9+NfbvK0TH7blaJYcJiW7ogYY -RTwOG61y78IV55Ze9/9M6A --> ssh-ed25519 /vwQcQ DKJKkbb0RO1jfSBH3KR0OGMfj0ZJQYVz7GDUMP/bJwE -vzNWlrkN3UYst11R3opQSdz/pnd48gWmsv3jv/g+3+A --> ssh-ed25519 0R97PA i+He+K6aBSItKmvA/2rvtS4IfqBK2rOYOBhTURMPqQ4 -k+FuDsFbS1MNT5vEzYSny80FBU+L+OdxMGEAT3e/VP0 +isST86UT1UMMOKiaTWaREFMn22H1MPDbLbuIeZzBhkTh0GnzmKKCxZV7+wPX4Woc +mq9FtkQBcbawrV20ZafF4bKqBl2ByuDmqe+VVWLchrR0DdkspsEBBHIw42rG79ZU +x+nNXnsDAFMdLRBEcNZSBVoGd1KhTuyoOM91dJ1IueJx3j8SA5JNCIG1Zf8vJKqX +WuZtjQiI949wzfU0WX+T5hT0uMzuzeIJ993vUJ2+2twN+KQnq0JYMTG26g40tSH+ +uAENRS6dwQep5BI5nnx1yKli3zX2Dc+4MLXiglKCxkyZ4sjYtfzMnlaAfpahyxVm +8i37qYGdyyxLPRWxS3SMUQ +-> ssh-ed25519 /vwQcQ Ig4WFFJ4Ex6cYghg14FJJfskUKyI5gWJwd5vn0XmDH8 +uHwxOnD8aVeG8YwiMhcj2BB6q3o+CfM6C7GP3jcXjyo +-> ssh-ed25519 0R97PA UvK0BtCJBVNnc2rcpz49iwdqmIt31aAXVFvTZw6/Fyk +fcZquflamVQnWRQVL+NsUwiQaJs9Qzi+Va5wrg0pa1k -> ssh-rsa jL+Elw -pex+P4zsbQhndeUZC/DsmRIYLaclvAMR75NxwQIhlzXTvARWfrClhTXmD0cmA2QM -hMvtDADwW/RRkFZuVKY/u6dA4/yZdua8by1RITrnLVAYDqr7H2u7bCozESJChJJw -XsNglDQ2Zkw9le4EjX0oO7hyt5eM07DYnq3nmFuqVUzWh1qvxOtcELkOE/fi0AwJ -RvrGbM27w0V6N7BYAefBbVc+OFbNg2ai0WECxlL4HXR9BgmVYGybYR+11Bu4scCM -mS9CKbW4UtVhK+tPVEgO3gymwv99PYFwcwvdGdyAAp9yVgjbKZt9Lm6J/eNpj/82 -YMYW6ZBK0jdtT4YSNu/88EXoQghoOyCEbFAYXoHiaNCiFxcKOOdY65B1SATi1Cuh -Qqo3Ejfx7waS3iArPFQFzS4X5ZKtj3GaHXKQkziD7GmP9cjGWt7S8Wh9pi/G6Bx1 -vGFhdfwtd+pGlbhrXd5jU6+UnVGtPBmgN8F/CpvqII4HN62Ds+npUs31uFGW1lzl -b4q+ij3s/Obt7HHCAhywXfnTDZ15WDcU8cWfSlsoTd63sc8INYXvDzFX46kax0E+ -yqGoFzYuke0FJxUf/tll4wv5aY9RcgAoHsDP7CL8nB1556NwVYFn8AujY87IiNAw -G/WSn9URjTgxW1YhiIvJ5L3mp/DjAZiakCU/85KEf08 --> ssh-ed25519 jIXfPA pPEvleLgX5FDDHbWlq6Szu61VMt4W7e3HrM5bYQA4QU -Ag8v967xi0AEbg8fiYAAmA36rkdZETCb3q/veoRdEbA --> ssh-ed25519 um7xWA gkxkaZftmupdeK1fsi7v59wAsM/Usc8SXjmxRkoBjmM -cjiizU43+Fed3vFqOpoePhicklpsSoAhEHZhHlkXr5Q ---- Xw/OABODZr4PGKpuNi03kdUptlyHF8hEK3+KtDulu7U -qk:hMq& eZI -qI[3v|DhQkbS|Sx"մP숸pEhưXU ssh-ed25519 jIXfPA HCM4xJkLVRfAvBzKyS5OZsbu91HPeZgx8sbwUP58LiY +GqJebsL5u0W66xU+cmXHbn7pYo7m//LggPIc3pWdJOQ +-> ssh-ed25519 um7xWA CCztuWK8B5skJS0xkpyo1cIci512Pmsi4Xp3M6Kf9DA +NUEcXszN91ycADY9oGKTGFWnCzv8ZIfEQ3zdkqm6/sg +--- eW8lDje70BhCexL17jXBtSA2X/jCnVwZUvackCd1bd0 +ޛy|L +GQ̎A-2ω}xh*qLp@~Ek._s&c) m~;E|t|oZ֦" \ No newline at end of file diff --git a/secrets/discourse-mail-password.age b/secrets/discourse-mail-password.age index 0810826..ff6cbb9 100644 --- a/secrets/discourse-mail-password.age +++ b/secrets/discourse-mail-password.age @@ -1,32 +1,33 @@ age-encryption.org/v1 --> ssh-ed25519 oDAQrw +aORlGGz/jqX0t4opGM5vMTgBKtAdL/z1KxfA4C43To -MNbp/PaSnpvyfZtmWWD6HYG3yHh2uWoXDY8V1Ft7+ac +-> ssh-ed25519 oDAQrw QeSmwUu/CvV+1LIgeeGEW6PFgOESwqTe1o7Ack1PWSs +Z4a8OsJAYdnJfG5IZq/ktMD9F3pfN3cjim8A2nZM5a4 -> ssh-rsa krWCLQ -2Px6I6WuRr10mMWlcUCAbnm6dyQa2hklmnwXtNPWXmKNPzHC6CGoye5dnOvkNKGN -5ekf2mK2ywzE2FQFVYOtY3Ss/60I4OTXmxNJ3qrCAU2z+f+53nAegguc3tB4Xvh8 -tQVQICaX1oVU/PD2SFX8QcTBORF2+Vc0Nd6fbuGl3dhSeESg7JLj5oaBCsthZD2J -U1ehTDS/+t6nogt1BdNZK10yFXRGk44jDMADMvvUPkIEW7mbvTFfMtXNhbemOn+X -oIXt8DNwzn3tF1IdOJ+vjtcU3gqYQYZVFKJV1DtQSA7i46Dl0J0+12+8/zKMX+yu -NKcaa/L1tz0lPX6ZeXhoOg --> ssh-ed25519 /vwQcQ QoEbQ1IA4EKR97Fqa/NQN9RDUEubeZbvxMcd0ZFhwyU -OAFrGGXCxDFcsalinhG3JcXUj+RJawl6UnSgZkBwBrk --> ssh-ed25519 0R97PA VkKhx5oVdrFjjSXwreIiGnH0nZEqY5Ls4OSpgpDn0jI -FC9SN+Woh6QBWV5r5TjtjFSp8mqw3LxgFJCQPjD8oss +GnsaM6qYR58TY9xfDvFOD9On6jHuU6M5ku5hQExsdGFXkp+pultLC4d31iO/9YAw +NC+DjYRfQRgiVl7Ik7l5EbsoJitBsyJqNm6KckLgMr+m36i2l7myrUCXvkj29WPY +s2uRmlo4Ai6pthu2nI7dJJ0Va/Z3WtXOb+q2QIapbB/Jb3mcvkVbKkJNnVWpZjri +pafvy1ZlbWtOIHUHfLCyBo3R2sDQUFR8LL5Ww93CzjbiyfBVe0lgBvJJJUoZ+7qj +ZPykqASPNsKW+IQXb90BZlpf9JRi9l8VawZ1OCasWZ8e7UBvxMp/mpcZ9mUWDE+g +uvcAaBjx0ZHsN1u+k3Sx1A +-> ssh-ed25519 /vwQcQ jCxoSbDPpFe7eHJEX3rov05XWPkWDZvIRbiWhvJmsAI ++ivctO51LaGMF9PJHGhwLRNXXxq8VZSfnbjHIU6lE4M +-> ssh-ed25519 0R97PA 0GSdrtGAYks1eSMQXispNZTaa+UIQbP0eAzRP/aoXS4 +MNmpUVS/D5AxNaKfvxlqd5WW2+0cHjDo7/74vCzPDMU -> ssh-rsa jL+Elw -UGQRNdtkdNbRVcoimpZsgBAGp95mtHwJ8V4CwlL82GqdIGM3LQDrCtPgFTC1C4Zx -lrgVaT/xi4WhRkkHCd0ZTzJ5i9NY9H/gnDaXKeKcTcQezH1yeMBLns92D2z7u0+1 -DyTzu8ZzZLvaej2b7gLU6u2BMC10AnBYP0LFuZlE3ndnr7ro9flKPb5A+IRbX339 -xkNlIOsTBdSfJJ27LKZVIOUS2Pxg1Kos9Wtbg1QsgeikdrA++QD0GhrRd+X4sFrf -Xfjq12XDakCfLmvi9QLJ2hy4X1glCO1lsBocDEaa2dGQMa2yVQi72z/92w83g98C -UO25VkpKwdGFBf8PfhKeEkdSQEJUNOe3mlTEvtpr5S3BM+//fSFHlaS1UMEeamC2 -OrcvTwdby9f4l0++9dEuPcgQYvMzbUndwFn2HI7a8fGSIF6iXeVdrl5zuR3Wgugr -ksMiQ6IyItrv/XlR5945dTxragWIwDeNJFE5EEYU3F7ryhT1FMKUWlt3lDazVuuj -PoLjUnXNhi7I+3XZ22e5P2BW7UmMbsPg3M9l+u+1U1vk/o28cZxrWV6VW4MB1Aiv -TXFXZSl123ag32PdKZ/dV7p4VC4hXEqyLA7qIrE9rFduEI5WQHqd1iL/pGxEOnuM -AOe/acgmB1hhVLCAwCCpERWyC1+Bn7UWRjrPKCLfqFc --> ssh-ed25519 jIXfPA NaF9Sg7UkShfSzE59iFwKF4WMsc0xtAejl+20EQBNzc -4CJqeGTxwcG0ObXeWATVKQkqlyaKZEaHFfGBOc6Xpok --> ssh-ed25519 um7xWA rlZ0xF57VNq1KHijaV9csD+Sq6cfp5DM9k1uuVOhLyY -7uYk1RIET7bkdXTHJdocqOiEtc6vqY4iOBB4cMJ7CyQ ---- FiH1HnZbKHvI9kclq8TrOFlQN6hvNd9M0mxdqjYM5gw - kj p [tϖU9dcڗNE#c|ۃ䋷`GWMy v$d \ No newline at end of file +LWL7LZ+16Bnk2o/5/2QJq9UHqdUBXndU/lx+UqGSemcEJqbuikWEq5Sv9jTgME2U +mQYMr10vGxyDo0mSB9pbYGB5fWlAi61UutQKJuuwHgETF56fVy7+fsRmtokIcG7C +dF2ooUsx5/Ul2V0o2v4KieOZhU4KvhPwHVUdP6SkWCtn5kxrmJkbOjOrRNOVZffz +yOuViZlLlFrtzqtuwBi6uHhoz5x0t+m/v1JM1/zHFI5I3JPEpHvRfVKp7K2dOT2T +nr7UotsoGD2+nqrSp4YVgNdwy6EgjbVKGKF2rDr1hcUyMV+L8vRmAxQhfAWJxPYh +jOgHWIWOCLQX/6MrBB4+gvyNvrZUw6nvDwbMiMlVn6MS6H395Sh42PtiCnx27pc4 +xIGB4Zaq+38SnkpjqLVwSUak7rC0FHHu6nTUIQPIktmz8C1BWpNpTL4mtuT30U45 +MVKB1taKk87XVpqzxOgQVHa3CZzjANm8cpip4yWkcEskhv+3OZjeZNgkUHHR1KGE +suZxJJTrhhabV1N69i5LqmXgr4BZcJEkkdoJ4bFUaPttwkm6aUDpdSJbRowyzDbX +DWzmG/P1IpTdRw/7lKR7WUDMjsUmdMwCWpwlNetHM9t9aS62GBT2JKtTWfAPwdWw +VeGpq5o2R2EBW7ioVO3P14Mk3duD/PB8H8xr+gEczbA +-> ssh-ed25519 jIXfPA UhMaLIx8smNo4NoGkyh7ODvqOqhql4bI+vZsz8WKugE +d1PVZiDWykjx86YDlChW1BrLU/NHbHRfnnJ+m/eCIGQ +-> ssh-ed25519 um7xWA rjgDG4Rpaq0R54at3sc8TSUM1ipBAUq5Zl/di7i7jFc +G8XdosKSH6xxMMWFD5DbtyDdgNvfegbw1zEmTMI/TfU +--- Unyyf+PuoDOyqfNttxSNFPJrbmkLATNL03J0DbR9AxU +~bɸ@;ٟ|+;C-a!3L>k +34lqMv]P-Y9 \ No newline at end of file diff --git a/secrets/matrix-shared-secret.age b/secrets/matrix-shared-secret.age index 3fe822573e5e77766d4c7d2738ce61c7789adfd2..3c8acce90b714d2dbdf2c0d4a22fa6627f26911d 100644 GIT binary patch literal 1825 zcmZXUOUUd78HT00ih|-ss0)8V%8~e+ncT-kC6~!$l1y%sdy9}v?wQ;tR~12pB6eGG zErOt{;#r77kt*eM(Ve*KstSs&2nV_;LQfa}Mt9%tf%ko%&rhQJsJe7ZbIa=Ln{}5! z;}*Do@viUF`zVAlY`eQV%pC4fcY-so4B75n%-vj7)|hT%-VUdX49dnywn$&;XDXx- zk0Jqr!iI^u`D)y6=wm6kFo=(*DJ+ph0()Tq7aL6we1&QBf!6i@UD4T$qyfd{X3l85 zk3Bxp^|U#|UJkK?hQ!b^_O7GL7dD$#B!DBa6qudIH>yWP16jMhIB+g7V8t@-$`ofE z?(Abr?A1gn;8X#vsHh1WeQd^54j|Kkor8jduE*s zWkPf3+~5%z(ySmH`d%`lx!QW$s;xGy6k})Zni{_0T{T`_>D>U-KEFz>=Sz@#%(&Q} zG_qy_tMRO|#))mNJAOcfRshF)NaL^wG?gf}w7q6f93yQf!0sIW!pO0mgBi54s8EZ( zcsNPpPB4(Gfu80})l79n!^HN|Q0Zheafd$aj0IbEb^+|k1>HE-!8x_#UUg-uK4)j@ zA?=55p>fhK+8ysfu9Am?6hQO^XSxr??Wxk>1d8OUwLOXUP8ya5WgMj(Xj z#0glQ&jWpqB#@brXt``!s?2Wi(X_m^{$JWg-w`Tx*Taq&dFkZbDvvNy^#a|(YY?_U zt=0`G*umDKrPBnlnFfr@bgKf241o? zic|ojGT=}q(=z7>(gGLZBKcz6_Zf`}SQG$~CYEZxa;b3cY^dRm6FovP~veWpbS z)SCm|W&ZZKbL_Kxbfh-1H}E=DeXY$V;xxmuq0(^_50uS@-YCWiiZc5X-O?n@h&v#) zwlEJbnP%Y;v}(gJdLwJ&jpeF?ERK?@F_t7}peCNyx)Th_*$JX>#?CC}p*JiA8i*gY znhKBzDh@~_vlf}>2%L+9D-vdLH3rF;N>tsCJYaw~`j}pI zFVB>=z9I5ZZkLM6WH919lu5k6O}ngNQH?Z?{45EJoDhyX?kc}ARC)vknuyFXjU#76 zx5n{wl&e-@^7Edn>nXQ#B)uB+vi1Ic%&$K=Ya`h-lOHlFov5}uv%5fTU*xZY6FyF@`(X>dpoIV@~e zgYsBb=%~W0WBIBSD({kHKbWF=?TGfEs=CE*x_BZ@UI2C{+GzU1bxNd7WLrXZYbQ6T zK#blzL$HNYxDA&H7Hs~{(fsJd&#Vdd&0ZL=dJ0O%vXfgBu6t-sdLQ(ypoV=lV(}}M zF@0VG9psJtHNs2G!n7N;$c28K?YW)AYQIgI9)Ss&bYvsEm=D%Ge1xGSW}CX3JyDpu zou`0Hx8*JlcevtAWVRH2h^3UaY^yvH;L%>DBS4Hg5vaV-S;ICiuNy8Ja3K@8l$l(e#e1Ls%BOzw_RDWce}l~r|MAr~zVrHD{`B*Yo`3u?@|A1N literal 1825 zcmZ9Lxy!@|8OAHcjfGZ=sK_TCxQ5T%m!gnNGRaJincSIlncT{ri&laz z9+ehah=quvxM-)vYGJXKZXvpb7FK>1U$cL~?|q-=_XKJD7}sIHwqxGZpKAIPnh@ae za}UFiJ;o7A5D4=iE_>2vA8>p>2$nrhNzzqYbR7bf_cvN~N|HMli@0^B5@y*kt1kei zGE8LaY6){8rwe5|==-MG7_k8K673nh)X)dAvq)<*2$+oCL4-$Cc;GyrsLSc58Ez`+WzbRF8F9G~GdZ)8&FQ z?rg^THK&`lRo)Dyts!|ma_4Y&$dzPw+sJUEw=H#4G#6la0_E&75sL?5n^dr6)eWzs3% z^|S;#Gq*9>z$kBX!MN@v2=+zuZlQ)tVs!zdzO|eMASkFzVzr_T#HEtUwW9?Ezh>ly zM`kz}Whif8-L|`wvN~ru`U58)L&3Z>&;^SvRszbM<1n|p5Xjua;UfZ8aJS9RX+L)h zO|u^A59;CWL5Jc#V&g=fH1j0)O)K5dbe;r9>4}51B^Fp9er^McR~Of*k$M3FY2wn* zJZVX`cSqz`vOpp1Vo66l-^?qL>>(dfHZh7J&uX4&|2L^nK%ts} z-kjUki$m~OMTZzVOH9IPyUsnt6`BUlZJ&&db+|X@h%NmbOpk_ZQlq((b7OhBKN5nlL@u_G_OUHT#BpV(1R7Pr&f#&t zAgif1g6um(>)lf!z4TRlwTSaYN9kcMA_jy;#OQ4e#h73!?2RHMg^_@8hEk2Ctd=M& zF>cPta%E{$CD^v)$1LSIMRB{`P6Lx%h)u-`&2S?ko}3|q7)5&@k{XOuDzAPfm*g#vbc z%y$xf!^>J{v0SnQDeYad3{^<6a<0XM`*XP%Bp@VfBb7tVbQ{MjJ8H!PR=dwP_Fs z$#j%yCuD5o$Ub}gywT#|B~PmXD@tVa=*cl@uyq>Y7m>KNQ_YNsC?U5{=-H>Yalf1| zz@Ye9L$cC{P$z~fFyv4od>i7uiseg~@i)HBJE^4c9sYLO{YEV=`yik6P~kULIx)(X zsEkemkMhhHVi80vWhT951)CST1J(eQncBFv-NVwD?PG}xAjQn0adG7&S&uZt>?PCJ zYYweT;PLVCAyGJC4({fOlu?J(R_eRcG?uYK_?=%c^(Kk?;{d~^57&wTv@ zfBxA^^UMGF(Bm80FFySy_ul-?U;fVe*H2#mm|rvB{o#-P`i(!n;D774FTe9e>^C3& p`>$U8>E->Oeb3L|_k&-&hJWV`|DQ+i`+s}odtX86*Z$PK@IUagWjX)= diff --git a/secrets/mautrix-telegram.age b/secrets/mautrix-telegram.age index 4cbe594..bc510f2 100644 --- a/secrets/mautrix-telegram.age +++ b/secrets/mautrix-telegram.age @@ -1,34 +1,33 @@ age-encryption.org/v1 --> ssh-ed25519 GxF6ZA pCpuN1PDIOfZ4ZewmIv4YWNAbWy3FHQsvMumPHpGOjw -nGgBgGw+65u7+tASWpfk30cc2TzgVJGFdDz6Ar/R+5s +-> ssh-ed25519 GxF6ZA y3IlZBxYfMnMpuWDjVDdT1zE6+bDWyl2pbxuUvwXbEI +N4VsbsnYD2b0TqSE1P2DzuaKT6gnYOjCA/I6USV5Ulc -> ssh-rsa krWCLQ -2oFukVzQIv5zXfd2pK3XwE8xHLCcV7+oORMdz+pTOry9d0u6VQhP5+aKd26/d9q3 -/WL8AszBl9d6czi99QKlliGyjw8K2vl3jOYZCTUJj25bBUsL0wt3Qok26Ywmgg2A -zTWT4AZqgdoQ886kiZFLEePAhVaJ1OOY4ucPkFynKRigqv4gULzwEQQDogLeY2sk -FuhJQlK6nhjakro4rfQxWVnciuPUK0kV0ULGZXrxN/vrpK9zV1CdBnjgjtehMfRA -qzf9FxMyxS/5RV+oCeFWS0jeEOoLun2lwUH8/rGlMeyU1Jq23q07bozWsUitcM2E -IfFMhX2QT/WzmSraBIzs7g --> ssh-ed25519 /vwQcQ OihcemSx1H5D32fB6XReME+z01e5f2wbEsjY4ou7Kgs -2zyndfT7gXpYmQzMmgXLx+1L0A6XftTGbDqfjeMAoaU --> ssh-ed25519 0R97PA OkPY1xsNp3YqSkuo5J+RqjIkBTua9H0CHunZaYvqfQo -8DFe5TUziFk9rbXa8nodX7rE62mh7DicWCJzEpLn2jE +MXO3xn7SQrbcha9SUyaujaAQH2GTfdsb2Ec42QC5B+BCiFEte48mHTzMUZXDiOF/ +00/f4iVLEx8WLRp7YU5Y7ipigGdN+1r3V+ZRLlm7JzoAJ+fFJmqd9ErIzy+trdjC +D3LtNsxwBqHAtXsaq42uF5oV08xvqmrfM6t4YywKsaENSWeRIglSY/ZwzyRugrTr +DmMHnL9PTYr0cnF1vb7hbbN+l01NfCc7MaLKLWA5i3kzuhzKAx48RhCf3eay/6/r +t4bcEAuvj24gaB0bU6Lom65PH/FL69bBtjrFWrPPQIbX88+z+md+nb8++k9NSTD7 +PeavB+zvA+PkXJMiTpVO8Q +-> ssh-ed25519 /vwQcQ Uvp34DlCMLtalncf41gr4qJL2tySq52jeM4K1DBPCSY +yIJfRyIlZ7ShYdTDgebYriHVySvqvuPZoNKATDceL4k +-> ssh-ed25519 0R97PA pOecxvmM8Xk2T8oEhugDCB152QYs0IPBHyDDrRFbySA +kuJF7YejPboLWTAc1oRIFS6gp/pe7gyuPIfsH49XLNc -> ssh-rsa jL+Elw -sJTALMXdOMyhtL8DD8Qyq587PQGyH/hTrMQ32pLbYnVwfX6wKvEBR7lyPMN3jLbk -56WhA9X8Op0zOCzJd1DldKZeGMqUK2fYcjv1JrhTlE0LfS7Y3W4zCEadVr6pbkXo -jc98q3RNdD6V4RqrdtndienJ9zM+vQnyxmkVZOl+z+yi/y7ceLUZcbVBChMTh/Ok -PiS60pk0NA9DFIds+IKVeKGA26iq0s2cjg0AUaicyV1LK4TF3LdLf4aanHt5QWE9 -ykQbtQLpp8DQifBNEl80YBg62YVz8f5B3/zBMjZaxIKIKXsdNTc7bqzbzPIhVUse -C+6Kutx2mskiWtMvYh15kkfbuBnI3wtV98IFLhFaZGRy0NBH3sguQvEUb2tNzyBT -F5hUqzXsKlmH4wKYAZV1JNwaEn1UzLx52+1Qq4V1VKimEc/kfTJpwSbWF76LLB0s -GVEWd5wpG2KDLKeIxFptPj+HpEAfRTE/MSW/9zz1V7Be1gk4ZFrQikj2LbY+MIvy -Z76xYvZA5RN+dHD+FuAhPzuEP8YpsBKAHiMbbKvwE4cIHuacgK6uiGVxNYEha/dJ -Met7RrF5kPSs4KrAmdFEq07ebfLoBD4DDbjJ3TDiFrap8kXLsekNNuz2By04TRZv -KCZGqR1c9EVGuAU5GgXHYsBcGUHBy5SoEpeuRVrpLz8 --> ssh-ed25519 jIXfPA bUYlIimYWfzWGWlmXx2cvbhFIwqJV/d1VrFvdAIBKw8 -pyb/vVXXcY3NuVWwRF9QTLQUUvXbKXU4TY6FU7nBanE --> ssh-ed25519 um7xWA DX1EN7dexMvKKuf69E2rnWtQDRXBMf5d5/l1ej/DPhQ -8qeFNTNnBfArNGwEIqlbmuWQz9T4oJhjdzScoyPeyyc ---- GqybhtQAk+XPOg90zmdeUUOSmT/cHs5RLgxnif8gTNM -ؗ-<ݔX?('aUI_xCRpOjCgjrݵ ^_>6H~Vf)9@e=|}\Tş340'M﹏"ܻ(kVLFUSAtԓysSQ,Or]I,}n!ve}ct&Հ)r!;Unquwᢵ.v^Xͪ9w qg.6!1A>0o2T5< >p~g9#uZ%@\lZhb -sn$hR%FmPńVIF%ywl0G)ç3M -0:*q1Ԙq'YG_i(k q[&,]ovz1vv \ No newline at end of file +Fb0wbIz3bKDqqlzXonleDkfK9CWiXwW5o9LkvVuCIYwq/o0F69Sn0itWeIo1cQ+s +fL5QEsRRbTsYZ63O8uJvdr2867wv0pzGffmErXv+rzVZoFMqq2swJ5Mn0yUXzItH +CPhRoLHfewMZVgE+fti4UigZxiOtoPR1+Nsmn2NcypNaz5Y+3J75zSHDCrcbJr0L +/U5ACXMPpFocZcL9CO6lySxJNyaOFyl8sX1azCjSeN3C87OMsRBQ7YO0/UL6FxWp +cwGTcd+sEgrf1bybR9p7khdsbPybpLzFtjcCpXjncdhagPG1241ohWGbCAx9bHtl +mSWhz89RCn6Tf00drosGdLyhKR7JfLr76mFdmU3xmKkZZu6xMkeoXDmUdtOd52ri +iF/OcHXFD/G4Vx2Fd8z1sTgIOYP0cDpzcn4JkWK9WjVyg9o4FNz98sfTSKEEcZc3 +dQSNpokXW9NH4VFrOkkdUzFn7kE7ZL5QcHmrxKQKS4gJ/Keq/g+WirV9bZ6w0QbQ +75JLS8/LAI8uXHzDYDVggp851eZIOTFL1SchjSjN/6BN+e/H02r9HbxxGnmOjNuw +0nC9R6t2nnwElw64TZyZru9VHPGGqWFmuyNOqj53Cn5PGNpy5hEiycMhT+Leh0De +hv/cQVlfFbZfRK8PMWrsZi0P88AG5XgzeEtnuRPqjqE +-> ssh-ed25519 jIXfPA j9rQS9GZYQTZli6ORRchsXBkoknUp7vSKLIAnOP7iD0 +nwrzn2WAkFVYNn9l7BgDdwgB52Fg0yF3zVKZfdgJXAw +-> ssh-ed25519 um7xWA /uvTBZxHjZwq8y/hpfKtZE91IXlPpw9lrwP05lzsC3o +kSqDREa/Sd4PHHsEnr49Vlis1bP/baFiRR8Adx4QvZw +--- 3t77fHsF25pYb0OuVHB4FlJQp8nPq2rD7wbNK4dzqcE +(RQ@6_:<]|oځhiy<{,a B~o͞rI;K13R2ĩ7Yk>/P9_ݺ 6c.ۮ>b;n1~7i%ۉwB6{|>t{cDeGqO``ؿ{&V]Y[VC/JM 7s7otbPץaPY7Yz^+b%lesfff0_k}qK>*;u#P2h9tVLnmv%}a>f(WGt+3d~e z;U$EL^^+U*++AVv$8ttUa~1M)Mrhj}i@kw+p$(7tsT};HVnU&lJ6jDO;6vSEkc8oEx2}6>aa_Qi=mFncV6FVh z?%eQdY4J)~atYiEHa5m+EZzwv5h2$xjo+cUYCv!s`#UDj{0Fb=M|!j$wG+?k!q>Vy zvR?aGSDUsOvsEvta>_&LoUjbu3BjNON!n1TxTSGzGrH+tP5Ew@Ty94XKCB$C;Kx={ z2GA9Ynb&NO5vF&tXG5m3$%_$ly#)B>2J!4Hpg7?D z=)$TM8(1RM@!M8i62F2?4nlTvKW0p5R&FB|n3yKFGEq#t z$Z^b0LsHWPt0?xt{0RS_wzFu`V4j%B5*eWqytBX}f?u;h;jeK-z1B&{+Y!Wwr-9l#uU6m=u#jg&GwKGc!O+3|GL?gPx=6JvY3#%E zO{5wt*y4^Caz#i23oBO^o)3wjCXrM=6dRVdjh6#A*Q=D7WCkKqb!%}ZwJhXBqdmFn z=Vqz59-l@x%t{m3UP8wGYT_s$xj}`y7y&WrC>81>v1>uF()Px@#>wLj6JgknN+?`6 zL8^ST&xs)LgzL!`y#7L_UF4N$=BgMB6o0?@oayXjs18E3Tq^`bn^*>El=nePD4mWY z`*lWEg-4w;mBw@xQ)I&~1t&&!;7me08wOJ^09qCXf5Z_+@7sDM9>JQ~1qdZ_XccPm z$(xQ^Y{4lEyxY7xDaLXrXqsF&#ST+EKHi|FERP_SwLuR7IMSDP&q|jOUMokoMkd>- z?Mddn<=zlfSggB(_F46oFcxMC083LFI+uA}Y}Iz?1brbmk2$JzC2+VyEr$`YM*_VB zZV~c$ z*^TL3Cl5)aB;joLpdHO?s!IJGnwA{{!xhWSBqw@cdz>E1m0V)R=`k`e$JksESEWPc z=Dzg!Wut`{B(GPKukroXnd#w7wWk4|EQeoTza7fA6DGp}rj3_y3ENKRMM%r_+9$W| z9G0^kE6R4x5EG+UMWHPtU2i)@^p>^VNy-0K^Fo_ya`Blbd&aSx>oK(R{dL?_qd&iP zGP53Z66jY^v_*;wHY&OnCN%Fzd}7r{TDcTTqmot32g7aG!`8E^lQwOwb7q-XKzpt2 z`S~dVQ3jVcMyJCXC$$Og3vO{q77r!~nD%tML~?Or@U!&m?I{wLo5(6=+|)%V`} z$v2+A@Uw5h@4oB&zWVMTzVy!@U4H(V?_WOZ2tR!jnt$-;>sMZ}zgdKT@#5FN{LQz% z_Cf3;zexZ1SM|NWx4$Yr@XlW{9(?ibPrv#S0?=aS literal 1809 zcmZ9MyX))-0fn)XfUxaFke`SM4tMjOB#n^Cd-6^uuR+AT-^nC1d3t3*6a;OxQEWsz z!L_hj1r?2hfnqBF^UQJf@yGc4s(G}+}w>o zvcWtotJ2m9og%hjTRPcf(Wn-J!hu#)I;iRe0c&n2#imRR*rW%3tH~sKGF1;UsRh4$3)5kuDmRs8}=S`nHpF#Zi zk;ik;ueYq7DKWM9)~}!_I5N4w@(U=Jmh6tM1%T1rB#ouq&k{!Vl^0ZL5I&@mdfSLtP2F>XI-oKs}&q=4CfT#k>2>j|I&W$7MvF| zHlOYty!8%aL3~xs4r;OjMA>5CZs=IAwP<-#+@XI!8CbT3J26C<^_(RrvM@P&D7Jy9= zVav}=@7f?8pGuo68O$lYLs>$IF{z@Fhe2#>x`-W>HVcxoK$;GAXw{n~%~}t1h!_mz za<9(|tE_}I(H`QTC_ERaxuPh>zS2pIgEczlalapC+D+WLZ7;V5wLAhd0WFz1yvGxQ zof@T19GnH|)8&HKR4kk@Y_pRyFHu%tGDLS>_W}(Ek-pc}eueK^wxPgQSvN70FAMH{3ATwwZ@|h=GMV2uTO#y!#MqLKp)%up1%o zvJ29PgK1<558@$a#$mikDpz!gh$LK_pM>jXx@ZcVkCH2i58;X}L7~N=ecUgP^R`JG zvy;_DXzlXucbKIplz!w14Zo)iceLxF2thX$a-b1AvW~J1gK*j?|V(g-#(P%^h67)Zr!`O0=cy5>)+p zp=P<&sg8G^J$_zICHdLMtLfDh)6a4`x~1Pp0&)`z7snw7rv~jJW=UIB7Mm<7wwgc; z<8h=l&VZQKv=>KDb#~dSu+m*zScm4yDTdTbt@>>KKW%D>)uW%<7)mpEsf5&mIGvVt zuY8P|7m-_eY$(%Mx#{F>UO|})uuNQQag$Zj)JRwKf^L44VXk6K$>G*k(OyH{+69S% z-hr1dUp^&aiS8ybONN>p_jwXaw7494>@sp2>7C@m7N(a?)KgOe{`~FfPw)No8^2?| zN)zdizyH>6|M>dT?<0Tz;=kTi#MjHurQi6k%6Go=!_R-}L+QoOzEFPi_rK_V^}!pT z`1dR|E7MqdFS6>`{zGiSFb+$%;)k?e)s+4%{MT>c=rdd{Nb~I Sp?>nw2iS|Z|N4iJtp5WBu41VG