oncall: init at 2.1.7; nixos/oncall: init (#388723)

Changed files
+783
nixos
doc
manual
release-notes
modules
services
web-apps
tests
pkgs
by-name
development
python-modules
falcon-cors
irisclient
top-level
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [vwifi](https://github.com/Raizo62/vwifi), a Wi-Fi simulator daemon leveraging the `mac80211_hwsim` and `vhost_vsock` kernel modules for efficient simulation of multi-node Wi-Fi networks. Available as {option}`services.vwifi`.
+
- [Oncall](https://oncall.tools), a web-based calendar tool designed for scheduling and managing on-call shifts. Available as [services.oncall](options.html#opt-services.oncall).
+
- [Homer](https://homer-demo.netlify.app/), a very simple static homepage for your server. Available as [services.homer](options.html#opt-services.homer).
- [Ghidra](https://ghidra-sre.org/), a software reverse engineering (SRE) suite of tools. Available as [programs.ghidra](options.html#opt-programs.ghidra).
+1
nixos/modules/module-list.nix
···
./services/web-apps/nostr-rs-relay.nix
./services/web-apps/ocis.nix
./services/web-apps/olivetin.nix
+
./services/web-apps/oncall.nix
./services/web-apps/onlyoffice.nix
./services/web-apps/open-web-calendar.nix
./services/web-apps/openvscode-server.nix
+203
nixos/modules/services/web-apps/oncall.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
+
cfg = config.services.oncall;
+
settingsFormat = pkgs.formats.yaml { };
+
configFile = settingsFormat.generate "oncall_extra_settings.yaml" cfg.settings;
+
+
in
+
{
+
options.services.oncall = {
+
+
enable = lib.mkEnableOption "Oncall web app";
+
+
package = lib.mkPackageOption pkgs "oncall" { };
+
+
database.createLocally = lib.mkEnableOption "Create the database and database user locally." // {
+
default = true;
+
};
+
+
settings = lib.mkOption {
+
type = lib.types.submodule {
+
freeformType = settingsFormat.type;
+
options = {
+
oncall_host = lib.mkOption {
+
type = lib.types.str;
+
default = "localhost";
+
description = "FQDN for the Oncall instance.";
+
};
+
db.conn = {
+
kwargs = {
+
user = lib.mkOption {
+
type = lib.types.str;
+
default = "oncall";
+
description = "Database user.";
+
};
+
host = lib.mkOption {
+
type = lib.types.str;
+
default = "localhost";
+
description = "Database host.";
+
};
+
database = lib.mkOption {
+
type = lib.types.str;
+
default = "oncall";
+
description = "Database name.";
+
};
+
};
+
str = lib.mkOption {
+
type = lib.types.str;
+
default = "%(scheme)s://%(user)s@%(host)s:%(port)s/%(database)s?charset=%(charset)s&unix_socket=/run/mysqld/mysqld.sock";
+
description = ''
+
Database connection scheme. The default specifies the
+
connection through a local socket.
+
'';
+
};
+
require_auth = lib.mkOption {
+
type = lib.types.bool;
+
default = true;
+
description = ''
+
Whether authentication is required to access the web app.
+
'';
+
};
+
};
+
};
+
};
+
default = { };
+
description = ''
+
Extra configuration options to append or override.
+
For available and default option values see
+
[upstream configuration file](https://github.com/linkedin/oncall/blob/master/configs/config.yaml)
+
and the administration part in the
+
[offical documentation](https://oncall.tools/docs/admin_guide.html).
+
'';
+
};
+
+
secretFile = lib.mkOption {
+
type = lib.types.pathWith {
+
inStore = false;
+
absolute = true;
+
};
+
example = "/run/keys/oncall-dbpassword";
+
description = ''
+
A YAML file containing secrets such as database or user passwords.
+
Some variables that can be considered secrets are:
+
+
- db.conn.kwargs.password:
+
Password used to authenticate to the database.
+
+
- session.encrypt_key:
+
Key for encrypting/signing session cookies.
+
Change to random long values in production.
+
+
- session.sign_key:
+
Key for encrypting/signing session cookies.
+
Change to random long values in production.
+
'';
+
};
+
+
};
+
+
config = lib.mkIf cfg.enable {
+
+
# Disable debug, only needed for development
+
services.oncall.settings = lib.mkMerge [
+
({
+
debug = lib.mkDefault false;
+
auth.debug = lib.mkDefault false;
+
})
+
];
+
+
services.uwsgi = {
+
enable = true;
+
plugins = [ "python3" ];
+
user = "oncall";
+
instance = {
+
type = "emperor";
+
vassals = {
+
oncall = {
+
type = "normal";
+
env = [
+
"PYTHONPATH=${pkgs.oncall.pythonPath}"
+
(
+
"ONCALL_EXTRA_CONFIG="
+
+ (lib.concatStringsSep "," (
+
[ configFile ] ++ lib.optional (cfg.secretFile != null) cfg.secretFile
+
))
+
)
+
"STATIC_ROOT=/var/lib/oncall"
+
];
+
module = "oncall.app:get_wsgi_app()";
+
socket = "${config.services.uwsgi.runDir}/oncall.sock";
+
socketGroup = "nginx";
+
immediate-gid = "nginx";
+
chmod-socket = "770";
+
pyargv = "${pkgs.oncall}/share/configs/config.yaml";
+
buffer-size = 32768;
+
};
+
};
+
};
+
};
+
+
services.nginx = {
+
enable = lib.mkDefault true;
+
virtualHosts."${cfg.settings.oncall_host}".locations = {
+
"/".extraConfig = "uwsgi_pass unix://${config.services.uwsgi.runDir}/oncall.sock;";
+
};
+
};
+
+
services.mysql = lib.mkIf cfg.database.createLocally {
+
enable = true;
+
package = lib.mkDefault pkgs.mariadb;
+
ensureDatabases = [ cfg.settings.db.conn.kwargs.database ];
+
ensureUsers = [
+
{
+
name = cfg.settings.db.conn.kwargs.user;
+
ensurePermissions = {
+
"${cfg.settings.db.conn.kwargs.database}.*" = "ALL PRIVILEGES";
+
};
+
}
+
];
+
};
+
+
users.users.oncall = {
+
group = "nginx";
+
isSystemUser = true;
+
};
+
+
systemd = {
+
services = {
+
uwsgi.serviceConfig.StateDirectory = "oncall";
+
oncall-setup-database = lib.mkIf cfg.database.createLocally {
+
description = "Set up Oncall database";
+
serviceConfig = {
+
Type = "oneshot";
+
RemainAfterExit = true;
+
};
+
requiredBy = [ "uwsgi.service" ];
+
after = [ "mysql.service" ];
+
script =
+
let
+
mysql = "${lib.getExe' config.services.mysql.package "mysql"}";
+
in
+
''
+
if [ ! -f /var/lib/oncall/.dbexists ]; then
+
# Load database schema provided with package
+
${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema.v0.sql
+
${mysql} ${cfg.settings.db.conn.kwargs.database} < ${cfg.package}/share/db/schema-update.v0-1602184489.sql
+
touch /var/lib/oncall/.dbexists
+
fi
+
'';
+
};
+
};
+
};
+
+
};
+
+
meta.maintainers = with lib.maintainers; [ onny ];
+
+
}
+1
nixos/tests/all-tests.nix
···
odoo = handleTest ./odoo.nix { };
odoo17 = handleTest ./odoo.nix { package = pkgs.odoo17; };
odoo16 = handleTest ./odoo.nix { package = pkgs.odoo16; };
+
oncall = runTest ./web-apps/oncall.nix;
# 9pnet_virtio used to mount /nix partition doesn't support
# hibernation. This test happens to work on x86_64-linux but
# not on other platforms.
+156
nixos/tests/web-apps/oncall.nix
···
+
{
+
lib,
+
pkgs,
+
config,
+
...
+
}:
+
let
+
ldapDomain = "example.org";
+
ldapSuffix = "dc=example,dc=org";
+
+
ldapRootUser = "root";
+
ldapRootPassword = "foobar23";
+
+
testUser = "myuser";
+
testPassword = "foobar23";
+
teamName = "myteam";
+
in
+
{
+
name = "oncall";
+
meta.maintainers = with lib.maintainers; [ onny ];
+
+
nodes = {
+
machine = {
+
virtualisation.memorySize = 2048;
+
+
environment.etc."oncall-secrets.yml".text = ''
+
auth:
+
ldap_bind_password: "${ldapRootPassword}"
+
'';
+
+
environment.systemPackages = [ pkgs.jq ];
+
+
services.oncall = {
+
enable = true;
+
settings = {
+
auth = {
+
module = "oncall.auth.modules.ldap_import";
+
ldap_url = "ldap://localhost";
+
ldap_user_suffix = "";
+
ldap_bind_user = "cn=${ldapRootUser},${ldapSuffix}";
+
ldap_base_dn = "ou=accounts,${ldapSuffix}";
+
ldap_search_filter = "(uid=%s)";
+
import_user = true;
+
attrs = {
+
username = "uid";
+
full_name = "cn";
+
email = "mail";
+
mobile = "telephoneNumber";
+
sms = "mobile";
+
};
+
};
+
};
+
secretFile = "/etc/oncall-secrets.yml";
+
};
+
+
services.openldap = {
+
enable = true;
+
settings = {
+
children = {
+
"cn=schema".includes = [
+
"${pkgs.openldap}/etc/schema/core.ldif"
+
"${pkgs.openldap}/etc/schema/cosine.ldif"
+
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+
"${pkgs.openldap}/etc/schema/nis.ldif"
+
];
+
"olcDatabase={1}mdb" = {
+
attrs = {
+
objectClass = [
+
"olcDatabaseConfig"
+
"olcMdbConfig"
+
];
+
olcDatabase = "{1}mdb";
+
olcDbDirectory = "/var/lib/openldap/db";
+
olcSuffix = ldapSuffix;
+
olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
+
olcRootPW = ldapRootPassword;
+
};
+
};
+
};
+
};
+
declarativeContents = {
+
${ldapSuffix} = ''
+
dn: ${ldapSuffix}
+
objectClass: top
+
objectClass: dcObject
+
objectClass: organization
+
o: ${ldapDomain}
+
+
dn: ou=accounts,${ldapSuffix}
+
objectClass: top
+
objectClass: organizationalUnit
+
+
dn: uid=${testUser},ou=accounts,${ldapSuffix}
+
objectClass: top
+
objectClass: inetOrgPerson
+
uid: ${testUser}
+
userPassword: ${testPassword}
+
cn: Test User
+
sn: User
+
mail: test@example.org
+
telephoneNumber: 012345678910
+
mobile: 012345678910
+
'';
+
};
+
};
+
};
+
};
+
+
testScript = ''
+
start_all()
+
machine.wait_for_unit("uwsgi.service")
+
machine.wait_for_unit("nginx.service")
+
machine.wait_for_file("/run/uwsgi/oncall.sock")
+
machine.wait_for_unit("oncall-setup-database.service")
+
+
with subtest("Home screen loads"):
+
machine.succeed(
+
"curl -sSfL http://[::1]:80 | grep '<title>Oncall</title>'"
+
)
+
+
with subtest("Staticfiles can be fetched"):
+
machine.wait_until_succeeds(
+
"curl -sSfL http://[::1]:80/static/bundles/libs.js"
+
)
+
+
with subtest("Staticfiles are generated"):
+
machine.succeed(
+
"test -e /var/lib/oncall/static/bundles/libs.js"
+
)
+
+
with subtest("Create and verify team via REST API"):
+
import json
+
+
# Log in and store the session cookie
+
login_response = machine.succeed("""
+
curl -sSfL -c cookies -X POST \
+
--data-raw 'username=${testUser}&password=${testPassword}' \
+
http://[::1]:80/login
+
""")
+
+
# Parse csrf token
+
login_response_data = json.loads(login_response)
+
csrf_token = login_response_data["csrf_token"]
+
+
# Create the team
+
machine.succeed(
+
f"""curl -sSfL -b cookies -X POST -H 'Content-Type: application/json' -H 'X-CSRF-Token: {csrf_token}' -d '{{"name": "${teamName}", "email": "test@example.com", "scheduling_timezone": "Europe/Berlin", "iris_enabled": false}}' http://[::1]:80/api/v0/teams/"""
+
)
+
+
# Query the created team
+
machine.succeed("""
+
curl -sSfL -b cookies http://[::1]:80/api/v0/teams/${teamName} | jq -e '.name == "${teamName}"'
+
""")
+
+
'';
+
}
+128
pkgs/by-name/on/oncall/package.nix
···
+
{
+
lib,
+
python3,
+
fetchFromGitHub,
+
fetchPypi,
+
oncall,
+
nixosTests,
+
+
# Override Python packages using
+
# self: super: { pkg = super.pkg.overridePythonAttrs (oldAttrs: { ... }); }
+
# Applied after defaultOverrides
+
packageOverrides ? self: super: { },
+
}:
+
let
+
defaultOverrides = [
+
# Override the version of some packages pinned in Oncall's setup.py
+
(self: super: {
+
# Support for Falcon 4.X missing
+
# https://github.com/linkedin/oncall/issues/430
+
falcon = super.falcon.overridePythonAttrs (oldAttrs: rec {
+
version = "3.1.3";
+
src = fetchFromGitHub {
+
owner = "falconry";
+
repo = "falcon";
+
tag = version;
+
hash = "sha256-7719gOM8WQVjODwOSo7HpH3HMFFeCGQQYBKktBAevig=";
+
};
+
});
+
})
+
];
+
+
python = python3.override {
+
self = python;
+
packageOverrides = lib.composeManyExtensions (defaultOverrides ++ [ packageOverrides ]);
+
};
+
in
+
python.pkgs.buildPythonApplication rec {
+
pname = "oncall";
+
version = "2.1.7";
+
format = "setuptools";
+
+
src = fetchFromGitHub {
+
owner = "linkedin";
+
repo = pname;
+
tag = "v${version}";
+
hash = "sha256-oqzU4UTpmAcZhqRilquxWQVyHv8bqq0AGraiSqwauiI=";
+
};
+
+
patches = [
+
# Add support for loading extra settings file
+
./support_extra_config.patch
+
+
# Support storing assets in custom state dir
+
./support_custom_state_dir.patch
+
+
# Log Python errors to uwsgi
+
./verbose_logging.patch
+
];
+
+
dependencies = with python.pkgs; [
+
beaker
+
falcon
+
falcon-cors
+
gevent
+
gunicorn
+
icalendar
+
irisclient
+
jinja2
+
phonenumbers
+
pymysql
+
python-ldap
+
pytz
+
pyyaml
+
ujson
+
webassets
+
];
+
+
postInstall = ''
+
mkdir "$out/share"
+
cp -r configs db "$out/share/"
+
'';
+
+
checkInputs = with python.pkgs; [
+
pytestCheckHook
+
pytest-mock
+
];
+
+
disabledTestPaths = [
+
# Tests require running web server
+
"e2e/test_audit.py"
+
"e2e/test_events.py"
+
"e2e/test_ical.py"
+
"e2e/test_login.py"
+
"e2e/test_notification.py"
+
"e2e/test_override.py"
+
"e2e/test_pin.py"
+
"e2e/test_populate.py"
+
"e2e/test_roles.py"
+
"e2e/test_roster_suggest.py"
+
"e2e/test_rosters.py"
+
"e2e/test_schedules.py"
+
"e2e/test_services.py"
+
"e2e/test_subscription.py"
+
"e2e/test_teams.py"
+
"e2e/test_users.py"
+
];
+
+
pythonImportsCheck = [
+
"oncall"
+
];
+
+
passthru = {
+
tests = {
+
inherit (nixosTests) oncall;
+
};
+
inherit python;
+
pythonPath = "${python.pkgs.makePythonPath dependencies}:${oncall}/${python.sitePackages}";
+
};
+
+
meta = {
+
description = "A calendar web-app designed for scheduling and managing on-call shifts";
+
homepage = "http://oncall.tools";
+
changelog = "https://github.com/linkedin/oncall/blob/${src.tag}/CHANGELOG.md";
+
license = lib.licenses.bsd2;
+
maintainers = with lib.maintainers; [ onny ];
+
mainProgram = "oncall";
+
};
+
}
+56
pkgs/by-name/on/oncall/support_custom_state_dir.patch
···
+
diff --git a/src/oncall/ui/__init__.py b/src/oncall/ui/__init__.py
+
index a94fb17..364404a 100644
+
--- a/src/oncall/ui/__init__.py
+
+++ b/src/oncall/ui/__init__.py
+
@@ -18,8 +18,12 @@ from webassets.ext.jinja2 import AssetsExtension
+
from webassets.script import CommandLineEnvironment
+
+
STATIC_ROOT = environ.get('STATIC_ROOT', path.abspath(path.dirname(__file__)))
+
+SOURCE_ROOT = path.abspath(path.dirname(__file__))
+
assets_env = AssetsEnvironment(path.join(STATIC_ROOT, 'static'),
+
url='/static')
+
+assets_env.cache = False
+
+assets_env.manifest = False
+
+assets_env.load_path = [ path.join(SOURCE_ROOT, 'static') ]
+
+
assets_env.register('libs', Bundle(
+
'js/jquery-3.3.1.min.js', 'js/handlebars-4.0.12.min.js', 'js/bootstrap.min.js',
+
@@ -45,7 +49,7 @@ logger = logging.getLogger('webassets')
+
logger.addHandler(logging.StreamHandler())
+
+
jinja2_env = Jinja2Environment(extensions=[AssetsExtension], autoescape=True)
+
-jinja2_env.loader = FileSystemLoader(path.join(STATIC_ROOT, 'templates'))
+
+jinja2_env.loader = FileSystemLoader(path.join(SOURCE_ROOT, 'templates'))
+
jinja2_env.assets_environment = assets_env
+
+
_filename_ascii_strip_re = re.compile(r'[^A-Za-z0-9_.-]')
+
@@ -113,14 +117,15 @@ def secure_filename(filename):
+
class StaticResource(object):
+
allow_no_auth = True
+
+
- def __init__(self, path):
+
+ def __init__(self, path, root):
+
self.path = path.lstrip('/')
+
+ self.root = root
+
+
def on_get(self, req, resp, filename):
+
suffix = path.splitext(req.path)[1]
+
resp.content_type = mimes.get(suffix, 'application/octet-stream')
+
+
- filepath = path.join(STATIC_ROOT, self.path, secure_filename(filename))
+
+ filepath = path.join(self.root, self.path, secure_filename(filename))
+
try:
+
resp.stream = open(filepath, 'rb')
+
resp.content_length = path.getsize(filepath)
+
@@ -153,8 +158,8 @@ def init(application, config):
+
+
application.add_sink(index, '/')
+
application.add_route('/static/bundles/{filename}',
+
- StaticResource('/static/bundles'))
+
+ StaticResource('/static/bundles', STATIC_ROOT))
+
application.add_route('/static/images/{filename}',
+
- StaticResource('/static/images'))
+
+ StaticResource('/static/images', SOURCE_ROOT))
+
application.add_route('/static/fonts/{filename}',
+
- StaticResource('/static/fonts'))
+
+ StaticResource('/static/fonts', SOURCE_ROOT))
+120
pkgs/by-name/on/oncall/support_extra_config.patch
···
+
diff --git a/src/oncall/bin/notifier.py b/src/oncall/bin/notifier.py
+
index 25142b8..cbc92aa 100644
+
--- a/src/oncall/bin/notifier.py
+
+++ b/src/oncall/bin/notifier.py
+
@@ -32,11 +32,29 @@ send_queue = queue.Queue()
+
+
default_timezone = None
+
+
+def merge_dict(extend_me, extend_by):
+
+ if isinstance(extend_by, dict):
+
+ for k, v in extend_by.items():
+
+ if isinstance(v, dict) and isinstance(extend_me.get(k), dict):
+
+ merge_dict(extend_me[k], v)
+
+ else:
+
+ extend_me[k] = v
+
+ return extend_me
+
+
def load_config_file(config_path):
+
with open(config_path, 'r', encoding='utf-8') as h:
+
config = yaml.safe_load(h)
+
+
+ # Check for extra config files from environment variable
+
+ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG')
+
+ if extra_config_paths:
+
+ for extra_path in extra_config_paths.split(','):
+
+ extra_path = extra_path.strip()
+
+ if os.path.isfile(extra_path):
+
+ with open(extra_path, 'r') as f:
+
+ extra_config = yaml.safe_load(f) or {}
+
+ config = merge_dict(config, extra_config)
+
+
+
if 'init_config_hook' in config:
+
try:
+
module = config['init_config_hook']
+
diff --git a/src/oncall/user_sync/ldap_sync.py b/src/oncall/user_sync/ldap_sync.py
+
index ef9a8ec..c5f027d 100644
+
--- a/src/oncall/user_sync/ldap_sync.py
+
+++ b/src/oncall/user_sync/ldap_sync.py
+
@@ -6,6 +6,7 @@ import time
+
import yaml
+
import logging
+
import ldap
+
+import os
+
+
from oncall import metrics
+
from ldap.controls import SimplePagedResultsControl
+
@@ -447,9 +448,28 @@ def main(config):
+
logger.info('Sleeping for %s seconds' % sleep_time)
+
sleep(sleep_time)
+
+
+def merge_dict(extend_me, extend_by):
+
+ if isinstance(extend_by, dict):
+
+ for k, v in extend_by.items():
+
+ if isinstance(v, dict) and isinstance(extend_me.get(k), dict):
+
+ merge_dict(extend_me[k], v)
+
+ else:
+
+ extend_me[k] = v
+
+ return extend_me
+
+
if __name__ == '__main__':
+
config_path = sys.argv[1]
+
with open(config_path, 'r', encoding='utf-8') as config_file:
+
config = yaml.safe_load(config_file)
+
+
+
+ # Check for extra config files from environment variable
+
+ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG')
+
+ if extra_config_paths:
+
+ for extra_path in extra_config_paths.split(','):
+
+ extra_path = extra_path.strip()
+
+ if os.path.isfile(extra_path):
+
+ with open(extra_path, 'r') as f:
+
+ extra_config = yaml.safe_load(f) or {}
+
+ config = merge_dict(config, extra_config)
+
+
+
main(config)
+
diff --git a/src/oncall/utils.py b/src/oncall/utils.py
+
index a0b695c..278ca1d 100644
+
--- a/src/oncall/utils.py
+
+++ b/src/oncall/utils.py
+
@@ -13,6 +13,7 @@ from pytz import timezone
+
from .constants import ONCALL_REMINDER
+
from . import constants
+
import re
+
+import os
+
+
invalid_char_reg = re.compile(r'[!"#%-,\.\/;->@\[-\^`\{-~]+')
+
DAY = 86400
+
@@ -27,10 +28,31 @@ def insert_notification(x, y):
+
def update_notification(x, y):
+
pass
+
+
+def merge_dict(extend_me, extend_by):
+
+ if isinstance(extend_by, dict):
+
+ for k, v in extend_by.items():
+
+ if isinstance(v, dict) and isinstance(extend_me.get(k), dict):
+
+ merge_dict(extend_me[k], v)
+
+ else:
+
+ extend_me[k] = v
+
+ return extend_me
+
+
def read_config(config_path):
+
+
+
with open(config_path, 'r', encoding='utf8') as config_file:
+
- return yaml.safe_load(config_file)
+
+ config = yaml.safe_load(config_file)
+
+
+
+ # Check for extra config files from environment variable
+
+ extra_config_paths = os.getenv('ONCALL_EXTRA_CONFIG')
+
+ if extra_config_paths:
+
+ for extra_path in extra_config_paths.split(','):
+
+ extra_path = extra_path.strip()
+
+ if os.path.isfile(extra_path):
+
+ with open(extra_path, 'r') as f:
+
+ extra_config = yaml.safe_load(f) or {}
+
+ config = merge_dict(config, extra_config)
+
+
+
+ return config
+
+
+
def create_notification(context, team_id, role_ids, type_name, users_involved, cursor, **kwargs):
+33
pkgs/by-name/on/oncall/verbose_logging.patch
···
+
diff --git a/src/oncall/app.py b/src/oncall/app.py
+
index 370fcf4..59f014e 100644
+
--- a/src/oncall/app.py
+
+++ b/src/oncall/app.py
+
@@ -62,9 +62,19 @@ class AuthMiddleware(object):
+
+
application = None
+
+
+def handle_uncaught_exception(req, resp, ex, params):
+
+ logging.exception('Unhandled error')
+
+ raise falcon.HTTPInternalServerError(title='App error')
+
+
+
+
+
+def handle_http_error(req, resp, ex, params):
+
+ logging.exception('HTTP error')
+
+ raise ex
+
+
+
+
def init_falcon_api(config):
+
global application
+
+
+
cors = CORS(allow_origins_list=config.get('allow_origins_list', []))
+
middlewares = [
+
SecurityHeaderMiddleware(),
+
@@ -74,6 +84,8 @@ def init_falcon_api(config):
+
if config.get('require_auth'):
+
middlewares.append(AuthMiddleware())
+
application = falcon.App(middleware=middlewares)
+
+ application.add_error_handler(falcon.HTTPError, handle_http_error)
+
+ application.add_error_handler(Exception, handle_uncaught_exception)
+
application.req_options.auto_parse_form_urlencoded = False
+
application.set_error_serializer(json_error_serializer)
+
application.req_options.strip_url_path_trailing_slash = True
+36
pkgs/development/python-modules/falcon-cors/default.nix
···
+
{
+
lib,
+
buildPythonPackage,
+
fetchFromGitHub,
+
setuptools,
+
falcon,
+
}:
+
+
buildPythonPackage rec {
+
pname = "falcon-cors";
+
version = "1.1.7";
+
+
src = fetchFromGitHub {
+
owner = "lwcolton";
+
repo = "falcon-cors";
+
tag = version;
+
hash = "sha256-jlEWP7gXbWfdY4coEIM6NWuBf4LOGbUAFMNvqip/FcA=";
+
};
+
+
build-system = [ setuptools ];
+
+
dependencies = [ falcon ];
+
+
# Test fail with falcon >= 4
+
# https://github.com/lwcolton/falcon-cors/issues/25
+
doCheck = false;
+
+
pythonImportsCheck = [ "falcon_cors" ];
+
+
meta = {
+
description = "CORS support for Falcon";
+
homepage = "https://github.com/lwcolton/falcon-cors";
+
license = lib.licenses.asl20;
+
maintainers = with lib.maintainers; [ onny ];
+
};
+
}
+43
pkgs/development/python-modules/irisclient/default.nix
···
+
{
+
lib,
+
buildPythonPackage,
+
setuptools,
+
fetchFromGitHub,
+
requests,
+
pytestCheckHook,
+
httmock,
+
pytest-mock,
+
}:
+
+
buildPythonPackage rec {
+
pname = "irisclient";
+
version = "1.2.0";
+
pyproject = true;
+
+
src = fetchFromGitHub {
+
owner = "houqp";
+
repo = "iris-python-client";
+
tag = "v${version}";
+
hash = "sha256-fXMw2BopkEqjklR6jr7QQIZyxLq6NHKm2rHwTCbtxR0=";
+
};
+
+
build-system = [ setuptools ];
+
+
dependencies = [ requests ];
+
+
checkInputs = [
+
httmock
+
pytestCheckHook
+
pytest-mock
+
];
+
+
pythonImportsCheck = [ "irisclient" ];
+
+
meta = {
+
description = "Python client for Iris REST api";
+
changelog = "https://github.com/houqp/iris-python-client/blob/v${src.tag}/HISTORY.rst";
+
homepage = "https://github.com/houqp/iris-python-client";
+
license = lib.licenses.bsd2;
+
maintainers = with lib.maintainers; [ onny ];
+
};
+
}
+4
pkgs/top-level/python-packages.nix
···
falcon = callPackage ../development/python-modules/falcon { };
+
falcon-cors = callPackage ../development/python-modules/falcon-cors { };
+
falconpy = callPackage ../development/python-modules/falconpy { };
faraday-agent-parameters-types =
···
ircstates = callPackage ../development/python-modules/ircstates { };
irctokens = callPackage ../development/python-modules/irctokens { };
+
+
irisclient = callPackage ../development/python-modules/irisclient { };
isal = callPackage ../development/python-modules/isal { };