arthexis 0.1.11__tar.gz → 0.1.13__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.11 → arthexis-0.1.13}/PKG-INFO +2 -2
- {arthexis-0.1.11 → arthexis-0.1.13}/README.md +1 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/arthexis.egg-info/PKG-INFO +2 -2
- {arthexis-0.1.11 → arthexis-0.1.13}/arthexis.egg-info/SOURCES.txt +19 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/config/asgi.py +15 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/config/celery.py +8 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/config/settings.py +49 -78
- arthexis-0.1.13/config/settings_helpers.py +109 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/admin.py +293 -78
- {arthexis-0.1.11 → arthexis-0.1.13}/core/apps.py +21 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/auto_upgrade.py +2 -2
- arthexis-0.1.13/core/form_fields.py +75 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/models.py +203 -47
- {arthexis-0.1.11 → arthexis-0.1.13}/core/reference_utils.py +1 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/core/release.py +42 -20
- {arthexis-0.1.11 → arthexis-0.1.13}/core/system.py +6 -3
- {arthexis-0.1.11 → arthexis-0.1.13}/core/tasks.py +92 -40
- {arthexis-0.1.11 → arthexis-0.1.13}/core/tests.py +75 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/core/views.py +178 -29
- arthexis-0.1.13/core/widgets.py +94 -0
- arthexis-0.1.13/nodes/admin.py +1161 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/apps.py +15 -0
- arthexis-0.1.13/nodes/feature_checks.py +133 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/models.py +287 -49
- arthexis-0.1.13/nodes/reports.py +411 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/tests.py +990 -42
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/urls.py +1 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/utils.py +32 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/views.py +173 -5
- arthexis-0.1.13/ocpp/admin.py +948 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/consumers.py +630 -15
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/evcs.py +7 -94
- arthexis-0.1.13/ocpp/evcs_discovery.py +158 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/models.py +236 -4
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/routing.py +4 -2
- arthexis-0.1.13/ocpp/simulator.py +745 -0
- arthexis-0.1.13/ocpp/status_display.py +26 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/store.py +110 -2
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/tests.py +1425 -33
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/transactions_io.py +27 -3
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/views.py +344 -38
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/admin.py +138 -3
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/context_processors.py +15 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/defaults.py +1 -2
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/forms.py +67 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/models.py +136 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/tests.py +379 -4
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/urls.py +1 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/views.py +64 -7
- {arthexis-0.1.11 → arthexis-0.1.13}/pyproject.toml +1 -1
- arthexis-0.1.13/tests/test_api_login_required.py +58 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_assistant_profile_admin.py +4 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_auto_upgrade_scheduler.py +3 -3
- arthexis-0.1.13/tests/test_build_pypi_command.py +71 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_celery_no_debug.py +10 -1
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_email_inbox.py +35 -0
- arthexis-0.1.13/tests/test_email_outbox_admin.py +124 -0
- arthexis-0.1.13/tests/test_invitation_login_view.py +125 -0
- arthexis-0.1.13/tests/test_manage_debug_flag.py +103 -0
- arthexis-0.1.13/tests/test_mcp_asgi.py +55 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_mcp_sigil_server.py +61 -1
- arthexis-0.1.13/tests/test_mcp_sigil_server_command.py +42 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_node_info_view.py +20 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_odoo_product.py +93 -3
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_pypi_token.py +16 -10
- arthexis-0.1.13/tests/test_release_build_flow.py +68 -0
- arthexis-0.1.13/tests/test_release_logs.py +30 -0
- arthexis-0.1.13/tests/test_release_packages.py +31 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_release_progress.py +15 -11
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_release_tasks.py +67 -12
- arthexis-0.1.13/tests/test_rfid_admin_scan_csrf.py +73 -0
- arthexis-0.1.13/tests/test_rfid_backend.py +63 -0
- arthexis-0.1.13/tests/test_settings_helpers.py +70 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_sigil_resolution.py +7 -2
- arthexis-0.1.13/tests/test_staff_required_decorator.py +57 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_temp_passwords.py +25 -0
- arthexis-0.1.13/tests/test_totp_admin.py +67 -0
- arthexis-0.1.13/tests/test_totp_backend.py +125 -0
- arthexis-0.1.13/tests/test_version_file.py +79 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_vscode_manage.py +16 -1
- arthexis-0.1.11/core/widgets.py +0 -51
- arthexis-0.1.11/nodes/admin.py +0 -588
- arthexis-0.1.11/ocpp/admin.py +0 -541
- arthexis-0.1.11/ocpp/simulator.py +0 -425
- arthexis-0.1.11/tests/test_email_outbox_admin.py +0 -43
- arthexis-0.1.11/tests/test_release_logs.py +0 -15
- arthexis-0.1.11/tests/test_rfid_admin_scan_csrf.py +0 -34
- arthexis-0.1.11/tests/test_scan_evcs_consoles_command.py +0 -103
- arthexis-0.1.11/tests/test_version_file.py +0 -23
- {arthexis-0.1.11 → arthexis-0.1.13}/LICENSE +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/arthexis.egg-info/dependency_links.txt +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/arthexis.egg-info/requires.txt +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/arthexis.egg-info/top_level.txt +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/__init__.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/active_app.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/auth_app.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/context_processors.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/horologia_app.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/loadenv.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/logging.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/middleware.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/offline.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/urls.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/config/wsgi.py +0 -0
- {arthexis-0.1.11/pages → arthexis-0.1.13/core}/__init__.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/admin_history.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/admindocs.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/backends.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/entity.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/environment.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/fields.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/github_helper.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/github_issues.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/lcd_screen.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/liveupdate.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/log_paths.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/mailer.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/middleware.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/notifications.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/public_wifi.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/sigil_builder.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/sigil_context.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/sigil_resolver.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/temp_passwords.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/test_system_info.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/tests_liveupdate.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/urls.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/user_data.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/workgroup_urls.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/core/workgroup_views.py +0 -0
- {arthexis-0.1.11/ocpp → arthexis-0.1.13/nodes}/__init__.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/actions.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/backends.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/dns.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/lcd.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/nodes/tasks.py +0 -0
- {arthexis-0.1.11/nodes → arthexis-0.1.13/ocpp}/__init__.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/apps.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/reference_utils.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/tasks.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/test_export_import.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/test_rfid.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/ocpp/urls.py +0 -0
- {arthexis-0.1.11/core → arthexis-0.1.13/pages}/__init__.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/apps.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/checks.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/middleware.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/pages/utils.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/setup.cfg +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_acronym_capitalization.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_client_report.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_doc_commands.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_doc_model_groups.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_history.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_index_actions.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_model_graph.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_object_history.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_profile_link.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_admin_system_stop.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_allowed_hosts_hostname.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_assistant_data_api.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_assistant_profile_api.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_awg_admin.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_benchmark_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_birthday_greetings.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_check_migrations_script.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_client_report_generation.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_client_report_schedule.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_csrf_failure.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_csrf_origin_subnet.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_dist_cleanup.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_email_collector.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_email_inbox_admin.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_email_inbox_search_action.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_email_profiles.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_env_refresh_clean.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_env_refresh_pip.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_env_refresh_unlink.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_experience_admin_group.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_fixture_presence.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_footer_no_references.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_footer_presence.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_footer_render.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_git_checks.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_github_issue_reporting.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_install_script.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_language_switch.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_lcd_check_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_lcd_smbus2.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_localhost_admin_backend.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_login_view_no_site.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_manage_debug.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_manuals.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_mcp_process.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_message_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_migrations.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_model_verbose_name_capitalization.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_network_setup_interactive.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_notifications_fallback.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_notify_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_ocpp_session_lock.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_odoo_profile.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_odoo_profile_admin.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_offline.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_package_admin_next_release.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_power_admin_group.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_profile_inline_deletion.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_projects_rfid.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_readme_language.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_reference_qr_code.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_reference_transaction_uuid.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_register_site_apps_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_release_checklist.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_release_manager_admin.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_request_invite.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_rfid_admin_reference_clear.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_rfid_background_reader.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_rfid_client_report.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_role_marker_filtering.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_seed_data.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_send_invite_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_shell_scripts.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_show_leads_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_sigil_builder.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_sites_utils.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_staff_login_net_message.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_switch_role_script.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_uninstall_script.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_update_fixtures_command.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_urls_autodiscover.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_user_data_admin.py +0 -0
- {arthexis-0.1.11 → arthexis-0.1.13}/tests/test_version_endpoint.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Summary: Django-based MESH system
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -115,7 +115,7 @@ Dynamic: license-file
|
|
|
115
115
|
|
|
116
116
|
# Arthexis Constellation
|
|
117
117
|
|
|
118
|
-
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://
|
|
118
|
+
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://github.com/arthexis/arthexis/blob/main/docs/development/ocpp-user-manual.md)
|
|
119
119
|
|
|
120
120
|
## Purpose
|
|
121
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Arthexis Constellation
|
|
2
2
|
|
|
3
|
-
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://
|
|
3
|
+
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://github.com/arthexis/arthexis/blob/main/docs/development/ocpp-user-manual.md)
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
4
4
|
Summary: Django-based MESH system
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -115,7 +115,7 @@ Dynamic: license-file
|
|
|
115
115
|
|
|
116
116
|
# Arthexis Constellation
|
|
117
117
|
|
|
118
|
-
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://
|
|
118
|
+
[](https://github.com/arthexis/arthexis/actions/workflows/coverage.yml) [](https://github.com/arthexis/arthexis/blob/main/docs/development/ocpp-user-manual.md)
|
|
119
119
|
|
|
120
120
|
## Purpose
|
|
121
121
|
|
|
@@ -18,6 +18,7 @@ config/logging.py
|
|
|
18
18
|
config/middleware.py
|
|
19
19
|
config/offline.py
|
|
20
20
|
config/settings.py
|
|
21
|
+
config/settings_helpers.py
|
|
21
22
|
config/urls.py
|
|
22
23
|
config/wsgi.py
|
|
23
24
|
core/__init__.py
|
|
@@ -30,6 +31,7 @@ core/backends.py
|
|
|
30
31
|
core/entity.py
|
|
31
32
|
core/environment.py
|
|
32
33
|
core/fields.py
|
|
34
|
+
core/form_fields.py
|
|
33
35
|
core/github_helper.py
|
|
34
36
|
core/github_issues.py
|
|
35
37
|
core/lcd_screen.py
|
|
@@ -63,8 +65,10 @@ nodes/admin.py
|
|
|
63
65
|
nodes/apps.py
|
|
64
66
|
nodes/backends.py
|
|
65
67
|
nodes/dns.py
|
|
68
|
+
nodes/feature_checks.py
|
|
66
69
|
nodes/lcd.py
|
|
67
70
|
nodes/models.py
|
|
71
|
+
nodes/reports.py
|
|
68
72
|
nodes/tasks.py
|
|
69
73
|
nodes/tests.py
|
|
70
74
|
nodes/urls.py
|
|
@@ -75,10 +79,12 @@ ocpp/admin.py
|
|
|
75
79
|
ocpp/apps.py
|
|
76
80
|
ocpp/consumers.py
|
|
77
81
|
ocpp/evcs.py
|
|
82
|
+
ocpp/evcs_discovery.py
|
|
78
83
|
ocpp/models.py
|
|
79
84
|
ocpp/reference_utils.py
|
|
80
85
|
ocpp/routing.py
|
|
81
86
|
ocpp/simulator.py
|
|
87
|
+
ocpp/status_display.py
|
|
82
88
|
ocpp/store.py
|
|
83
89
|
ocpp/tasks.py
|
|
84
90
|
ocpp/test_export_import.py
|
|
@@ -111,6 +117,7 @@ tests/test_admin_object_history.py
|
|
|
111
117
|
tests/test_admin_profile_link.py
|
|
112
118
|
tests/test_admin_system_stop.py
|
|
113
119
|
tests/test_allowed_hosts_hostname.py
|
|
120
|
+
tests/test_api_login_required.py
|
|
114
121
|
tests/test_assistant_data_api.py
|
|
115
122
|
tests/test_assistant_profile_admin.py
|
|
116
123
|
tests/test_assistant_profile_api.py
|
|
@@ -118,6 +125,7 @@ tests/test_auto_upgrade_scheduler.py
|
|
|
118
125
|
tests/test_awg_admin.py
|
|
119
126
|
tests/test_benchmark_command.py
|
|
120
127
|
tests/test_birthday_greetings.py
|
|
128
|
+
tests/test_build_pypi_command.py
|
|
121
129
|
tests/test_celery_no_debug.py
|
|
122
130
|
tests/test_check_migrations_script.py
|
|
123
131
|
tests/test_client_report_generation.py
|
|
@@ -142,15 +150,19 @@ tests/test_footer_render.py
|
|
|
142
150
|
tests/test_git_checks.py
|
|
143
151
|
tests/test_github_issue_reporting.py
|
|
144
152
|
tests/test_install_script.py
|
|
153
|
+
tests/test_invitation_login_view.py
|
|
145
154
|
tests/test_language_switch.py
|
|
146
155
|
tests/test_lcd_check_command.py
|
|
147
156
|
tests/test_lcd_smbus2.py
|
|
148
157
|
tests/test_localhost_admin_backend.py
|
|
149
158
|
tests/test_login_view_no_site.py
|
|
150
159
|
tests/test_manage_debug.py
|
|
160
|
+
tests/test_manage_debug_flag.py
|
|
151
161
|
tests/test_manuals.py
|
|
162
|
+
tests/test_mcp_asgi.py
|
|
152
163
|
tests/test_mcp_process.py
|
|
153
164
|
tests/test_mcp_sigil_server.py
|
|
165
|
+
tests/test_mcp_sigil_server_command.py
|
|
154
166
|
tests/test_message_command.py
|
|
155
167
|
tests/test_migrations.py
|
|
156
168
|
tests/test_model_verbose_name_capitalization.py
|
|
@@ -172,28 +184,34 @@ tests/test_readme_language.py
|
|
|
172
184
|
tests/test_reference_qr_code.py
|
|
173
185
|
tests/test_reference_transaction_uuid.py
|
|
174
186
|
tests/test_register_site_apps_command.py
|
|
187
|
+
tests/test_release_build_flow.py
|
|
175
188
|
tests/test_release_checklist.py
|
|
176
189
|
tests/test_release_logs.py
|
|
177
190
|
tests/test_release_manager_admin.py
|
|
191
|
+
tests/test_release_packages.py
|
|
178
192
|
tests/test_release_progress.py
|
|
179
193
|
tests/test_release_tasks.py
|
|
180
194
|
tests/test_request_invite.py
|
|
181
195
|
tests/test_rfid_admin_reference_clear.py
|
|
182
196
|
tests/test_rfid_admin_scan_csrf.py
|
|
197
|
+
tests/test_rfid_backend.py
|
|
183
198
|
tests/test_rfid_background_reader.py
|
|
184
199
|
tests/test_rfid_client_report.py
|
|
185
200
|
tests/test_role_marker_filtering.py
|
|
186
|
-
tests/test_scan_evcs_consoles_command.py
|
|
187
201
|
tests/test_seed_data.py
|
|
188
202
|
tests/test_send_invite_command.py
|
|
203
|
+
tests/test_settings_helpers.py
|
|
189
204
|
tests/test_shell_scripts.py
|
|
190
205
|
tests/test_show_leads_command.py
|
|
191
206
|
tests/test_sigil_builder.py
|
|
192
207
|
tests/test_sigil_resolution.py
|
|
193
208
|
tests/test_sites_utils.py
|
|
194
209
|
tests/test_staff_login_net_message.py
|
|
210
|
+
tests/test_staff_required_decorator.py
|
|
195
211
|
tests/test_switch_role_script.py
|
|
196
212
|
tests/test_temp_passwords.py
|
|
213
|
+
tests/test_totp_admin.py
|
|
214
|
+
tests/test_totp_backend.py
|
|
197
215
|
tests/test_uninstall_script.py
|
|
198
216
|
tests/test_update_fixtures_command.py
|
|
199
217
|
tests/test_urls_autodiscover.py
|
|
@@ -9,21 +9,35 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
|
|
9
9
|
|
|
10
10
|
import os
|
|
11
11
|
from config.loadenv import loadenv
|
|
12
|
+
from typing import Any, Awaitable, Callable, Dict, MutableMapping
|
|
12
13
|
from channels.auth import AuthMiddlewareStack
|
|
13
14
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
14
15
|
from django.core.asgi import get_asgi_application
|
|
15
16
|
import ocpp.routing
|
|
16
17
|
|
|
18
|
+
from core.mcp.asgi import application as mcp_application
|
|
19
|
+
from core.mcp.asgi import is_mcp_scope
|
|
20
|
+
|
|
17
21
|
loadenv()
|
|
18
22
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
19
23
|
|
|
20
24
|
django_asgi_app = get_asgi_application()
|
|
21
25
|
|
|
26
|
+
Scope = MutableMapping[str, Any]
|
|
27
|
+
Receive = Callable[[], Awaitable[Dict[str, Any]]]
|
|
28
|
+
Send = Callable[[Dict[str, Any]], Awaitable[None]]
|
|
29
|
+
|
|
22
30
|
websocket_patterns = ocpp.routing.websocket_urlpatterns
|
|
23
31
|
|
|
32
|
+
async def http_application(scope: Scope, receive: Receive, send: Send) -> None:
|
|
33
|
+
if is_mcp_scope(scope):
|
|
34
|
+
await mcp_application(scope, receive, send)
|
|
35
|
+
else:
|
|
36
|
+
await django_asgi_app(scope, receive, send)
|
|
37
|
+
|
|
24
38
|
application = ProtocolTypeRouter(
|
|
25
39
|
{
|
|
26
|
-
"http":
|
|
40
|
+
"http": http_application,
|
|
27
41
|
"websocket": AuthMiddlewareStack(URLRouter(websocket_patterns)),
|
|
28
42
|
}
|
|
29
43
|
)
|
|
@@ -9,7 +9,14 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
|
|
|
9
9
|
|
|
10
10
|
# When running on production-oriented nodes, avoid Celery debug mode.
|
|
11
11
|
NODE_ROLE = os.environ.get("NODE_ROLE", "")
|
|
12
|
-
|
|
12
|
+
PRODUCTION_ROLES = {
|
|
13
|
+
"constellation",
|
|
14
|
+
"satellite",
|
|
15
|
+
"control",
|
|
16
|
+
"terminal",
|
|
17
|
+
"gateway",
|
|
18
|
+
}
|
|
19
|
+
if NODE_ROLE.lower() in PRODUCTION_ROLES:
|
|
13
20
|
for var in ["CELERY_TRACE_APP", "CELERY_DEBUG"]:
|
|
14
21
|
os.environ.pop(var, None)
|
|
15
22
|
os.environ.setdefault("CELERY_LOG_LEVEL", "INFO")
|
|
@@ -22,61 +22,24 @@ from celery.schedules import crontab
|
|
|
22
22
|
from django.http import request as http_request
|
|
23
23
|
from django.http.request import split_domain_port
|
|
24
24
|
from django.middleware.csrf import CsrfViewMiddleware
|
|
25
|
-
from django.core.exceptions import DisallowedHost
|
|
25
|
+
from django.core.exceptions import DisallowedHost, ImproperlyConfigured
|
|
26
26
|
from django.contrib.sites import shortcuts as sites_shortcuts
|
|
27
27
|
from django.contrib.sites.requests import RequestSite
|
|
28
|
-
from django.core.management.utils import get_random_secret_key
|
|
29
28
|
from urllib.parse import urlsplit
|
|
30
29
|
import django.utils.encoding as encoding
|
|
31
30
|
|
|
31
|
+
from config.settings_helpers import (
|
|
32
|
+
extract_ip_from_host,
|
|
33
|
+
install_validate_host_with_subnets,
|
|
34
|
+
load_secret_key,
|
|
35
|
+
strip_ipv6_brackets,
|
|
36
|
+
)
|
|
37
|
+
|
|
32
38
|
if not hasattr(encoding, "force_text"): # pragma: no cover - Django>=5 compatibility
|
|
33
39
|
from django.utils.encoding import force_str
|
|
34
40
|
|
|
35
41
|
encoding.force_text = force_str
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
_original_validate_host = http_request.validate_host
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _strip_ipv6_brackets(host: str) -> str:
|
|
43
|
-
if host.startswith("[") and host.endswith("]"):
|
|
44
|
-
return host[1:-1]
|
|
45
|
-
return host
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _extract_ip_from_host(host: str):
|
|
49
|
-
"""Return an :mod:`ipaddress` object for ``host`` when possible."""
|
|
50
|
-
|
|
51
|
-
candidate = _strip_ipv6_brackets(host)
|
|
52
|
-
try:
|
|
53
|
-
return ipaddress.ip_address(candidate)
|
|
54
|
-
except ValueError:
|
|
55
|
-
domain, _port = split_domain_port(host)
|
|
56
|
-
if domain and domain != host:
|
|
57
|
-
candidate = _strip_ipv6_brackets(domain)
|
|
58
|
-
try:
|
|
59
|
-
return ipaddress.ip_address(candidate)
|
|
60
|
-
except ValueError:
|
|
61
|
-
return None
|
|
62
|
-
return None
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _validate_host_with_subnets(host, allowed_hosts):
|
|
66
|
-
ip = _extract_ip_from_host(host)
|
|
67
|
-
if ip is None:
|
|
68
|
-
return _original_validate_host(host, allowed_hosts)
|
|
69
|
-
for pattern in allowed_hosts:
|
|
70
|
-
try:
|
|
71
|
-
network = ipaddress.ip_network(pattern)
|
|
72
|
-
except ValueError:
|
|
73
|
-
continue
|
|
74
|
-
if ip in network:
|
|
75
|
-
return True
|
|
76
|
-
return _original_validate_host(host, allowed_hosts)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
http_request.validate_host = _validate_host_with_subnets
|
|
42
|
+
install_validate_host_with_subnets()
|
|
80
43
|
|
|
81
44
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
82
45
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
@@ -96,29 +59,7 @@ with contextlib.suppress(FileNotFoundError):
|
|
|
96
59
|
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
|
97
60
|
|
|
98
61
|
# SECURITY WARNING: keep the secret key used in production secret!
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _load_secret_key() -> str:
|
|
102
|
-
for env_var in ("DJANGO_SECRET_KEY", "SECRET_KEY"):
|
|
103
|
-
value = os.environ.get(env_var)
|
|
104
|
-
if value:
|
|
105
|
-
return value
|
|
106
|
-
|
|
107
|
-
secret_file = BASE_DIR / "locks" / "django-secret.key"
|
|
108
|
-
with contextlib.suppress(OSError):
|
|
109
|
-
stored_key = secret_file.read_text(encoding="utf-8").strip()
|
|
110
|
-
if stored_key:
|
|
111
|
-
return stored_key
|
|
112
|
-
|
|
113
|
-
generated_key = get_random_secret_key()
|
|
114
|
-
with contextlib.suppress(OSError):
|
|
115
|
-
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
-
secret_file.write_text(generated_key, encoding="utf-8")
|
|
117
|
-
|
|
118
|
-
return generated_key
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
SECRET_KEY = _load_secret_key()
|
|
62
|
+
SECRET_KEY = load_secret_key(BASE_DIR)
|
|
122
63
|
|
|
123
64
|
# SECURITY WARNING: don't run with debug turned on in production!
|
|
124
65
|
|
|
@@ -258,7 +199,7 @@ def _normalize_origin_tuple(scheme: str | None, host: str) -> tuple[str, str, st
|
|
|
258
199
|
if not scheme or scheme.lower() not in {"http", "https"}:
|
|
259
200
|
return None
|
|
260
201
|
domain, port = split_domain_port(host)
|
|
261
|
-
normalized_host =
|
|
202
|
+
normalized_host = strip_ipv6_brackets(domain.strip().lower())
|
|
262
203
|
if not normalized_host:
|
|
263
204
|
return None
|
|
264
205
|
normalized_port = port.strip() if isinstance(port, str) else port
|
|
@@ -325,13 +266,13 @@ def _origin_verified_with_subnets(self, request):
|
|
|
325
266
|
if normalized_origin is None:
|
|
326
267
|
return _original_origin_verified(self, request)
|
|
327
268
|
|
|
328
|
-
origin_ip =
|
|
269
|
+
origin_ip = extract_ip_from_host(normalized_origin[1])
|
|
329
270
|
|
|
330
271
|
for candidate in _candidate_origin_tuples(request, allowed_hosts):
|
|
331
272
|
if candidate == normalized_origin:
|
|
332
273
|
return True
|
|
333
274
|
|
|
334
|
-
candidate_ip =
|
|
275
|
+
candidate_ip = extract_ip_from_host(candidate[1])
|
|
335
276
|
if origin_ip and candidate_ip:
|
|
336
277
|
for pattern in allowed_hosts:
|
|
337
278
|
try:
|
|
@@ -367,13 +308,13 @@ def _check_referer_with_forwarded(self, request):
|
|
|
367
308
|
return _original_check_referer(self, request)
|
|
368
309
|
|
|
369
310
|
allowed_hosts = _get_allowed_hosts()
|
|
370
|
-
referer_ip =
|
|
311
|
+
referer_ip = extract_ip_from_host(normalized_referer[1])
|
|
371
312
|
|
|
372
313
|
for candidate in _candidate_origin_tuples(request, allowed_hosts):
|
|
373
314
|
if candidate == normalized_referer:
|
|
374
315
|
return
|
|
375
316
|
|
|
376
|
-
candidate_ip =
|
|
317
|
+
candidate_ip = extract_ip_from_host(candidate[1])
|
|
377
318
|
if referer_ip and candidate_ip:
|
|
378
319
|
for pattern in allowed_hosts:
|
|
379
320
|
try:
|
|
@@ -524,6 +465,7 @@ MCP_SIGIL_SERVER = {
|
|
|
524
465
|
"required_scopes": ["sigils:read"],
|
|
525
466
|
"issuer_url": os.environ.get("MCP_SIGIL_ISSUER_URL"),
|
|
526
467
|
"resource_server_url": os.environ.get("MCP_SIGIL_RESOURCE_URL"),
|
|
468
|
+
"mount_path": os.environ.get("MCP_SIGIL_MOUNT_PATH"),
|
|
527
469
|
}
|
|
528
470
|
|
|
529
471
|
|
|
@@ -544,8 +486,16 @@ OTP_TOTP_ISSUER = os.environ.get("OTP_TOTP_ISSUER", "Arthexis")
|
|
|
544
486
|
# Database
|
|
545
487
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
|
|
546
488
|
|
|
489
|
+
FORCED_DB_BACKEND = os.environ.get("ARTHEXIS_FORCE_DB_BACKEND", "").strip().lower()
|
|
490
|
+
if FORCED_DB_BACKEND and FORCED_DB_BACKEND not in {"sqlite", "postgres"}:
|
|
491
|
+
raise ImproperlyConfigured(
|
|
492
|
+
"ARTHEXIS_FORCE_DB_BACKEND must be 'sqlite' or 'postgres' when defined."
|
|
493
|
+
)
|
|
494
|
+
|
|
547
495
|
|
|
548
496
|
def _postgres_available() -> bool:
|
|
497
|
+
if FORCED_DB_BACKEND == "sqlite":
|
|
498
|
+
return False
|
|
549
499
|
try:
|
|
550
500
|
import psycopg
|
|
551
501
|
except Exception:
|
|
@@ -566,7 +516,15 @@ def _postgres_available() -> bool:
|
|
|
566
516
|
return False
|
|
567
517
|
|
|
568
518
|
|
|
569
|
-
if
|
|
519
|
+
if FORCED_DB_BACKEND == "postgres":
|
|
520
|
+
_use_postgres = True
|
|
521
|
+
elif FORCED_DB_BACKEND == "sqlite":
|
|
522
|
+
_use_postgres = False
|
|
523
|
+
else:
|
|
524
|
+
_use_postgres = _postgres_available()
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
if _use_postgres:
|
|
570
528
|
DATABASES = {
|
|
571
529
|
"default": {
|
|
572
530
|
"ENGINE": "django.db.backends.postgresql",
|
|
@@ -582,10 +540,16 @@ if _postgres_available():
|
|
|
582
540
|
}
|
|
583
541
|
}
|
|
584
542
|
else:
|
|
543
|
+
_sqlite_override = os.environ.get("ARTHEXIS_SQLITE_PATH")
|
|
544
|
+
if _sqlite_override:
|
|
545
|
+
SQLITE_DB_PATH = Path(_sqlite_override)
|
|
546
|
+
else:
|
|
547
|
+
SQLITE_DB_PATH = BASE_DIR / "db.sqlite3"
|
|
548
|
+
|
|
585
549
|
DATABASES = {
|
|
586
550
|
"default": {
|
|
587
551
|
"ENGINE": "django.db.backends.sqlite3",
|
|
588
|
-
"NAME":
|
|
552
|
+
"NAME": SQLITE_DB_PATH,
|
|
589
553
|
"OPTIONS": {"timeout": 60},
|
|
590
554
|
"TEST": {"NAME": BASE_DIR / "test_db.sqlite3"},
|
|
591
555
|
}
|
|
@@ -625,6 +589,8 @@ LANGUAGES = [
|
|
|
625
589
|
|
|
626
590
|
LOCALE_PATHS = [BASE_DIR / "locale"]
|
|
627
591
|
|
|
592
|
+
FORMAT_MODULE_PATH = ["config.formats"]
|
|
593
|
+
|
|
628
594
|
TIME_ZONE = "America/Monterrey"
|
|
629
595
|
|
|
630
596
|
USE_I18N = True
|
|
@@ -685,10 +651,15 @@ LOGGING = {
|
|
|
685
651
|
"backupCount": 7,
|
|
686
652
|
"encoding": "utf-8",
|
|
687
653
|
"formatter": "standard",
|
|
688
|
-
}
|
|
654
|
+
},
|
|
655
|
+
"console": {
|
|
656
|
+
"class": "logging.StreamHandler",
|
|
657
|
+
"level": "ERROR",
|
|
658
|
+
"formatter": "standard",
|
|
659
|
+
},
|
|
689
660
|
},
|
|
690
661
|
"root": {
|
|
691
|
-
"handlers": ["file"],
|
|
662
|
+
"handlers": ["file", "console"],
|
|
692
663
|
"level": "DEBUG",
|
|
693
664
|
},
|
|
694
665
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Utility helpers shared by :mod:`config.settings` and related tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import ipaddress
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Mapping, MutableMapping
|
|
10
|
+
|
|
11
|
+
from django.core.management.utils import get_random_secret_key
|
|
12
|
+
from django.http import request as http_request
|
|
13
|
+
from django.http.request import split_domain_port
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"extract_ip_from_host",
|
|
18
|
+
"install_validate_host_with_subnets",
|
|
19
|
+
"load_secret_key",
|
|
20
|
+
"strip_ipv6_brackets",
|
|
21
|
+
"validate_host_with_subnets",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def strip_ipv6_brackets(host: str) -> str:
|
|
26
|
+
"""Return ``host`` without IPv6 URL literal brackets."""
|
|
27
|
+
|
|
28
|
+
if host.startswith("[") and host.endswith("]"):
|
|
29
|
+
return host[1:-1]
|
|
30
|
+
return host
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_ip_from_host(host: str):
|
|
34
|
+
"""Return an :mod:`ipaddress` object for ``host`` when possible."""
|
|
35
|
+
|
|
36
|
+
candidate = strip_ipv6_brackets(host)
|
|
37
|
+
try:
|
|
38
|
+
return ipaddress.ip_address(candidate)
|
|
39
|
+
except ValueError:
|
|
40
|
+
domain, _port = split_domain_port(host)
|
|
41
|
+
if domain and domain != host:
|
|
42
|
+
candidate = strip_ipv6_brackets(domain)
|
|
43
|
+
try:
|
|
44
|
+
return ipaddress.ip_address(candidate)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return None
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_host_with_subnets(host, allowed_hosts, original_validate=None):
|
|
51
|
+
"""Extend Django's host validation to honor subnet CIDR notation."""
|
|
52
|
+
|
|
53
|
+
if original_validate is None:
|
|
54
|
+
original_validate = http_request.validate_host
|
|
55
|
+
|
|
56
|
+
ip = extract_ip_from_host(host)
|
|
57
|
+
if ip is None:
|
|
58
|
+
return original_validate(host, allowed_hosts)
|
|
59
|
+
|
|
60
|
+
for pattern in allowed_hosts:
|
|
61
|
+
try:
|
|
62
|
+
network = ipaddress.ip_network(pattern)
|
|
63
|
+
except ValueError:
|
|
64
|
+
continue
|
|
65
|
+
if ip in network:
|
|
66
|
+
return True
|
|
67
|
+
return original_validate(host, allowed_hosts)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def install_validate_host_with_subnets() -> None:
|
|
71
|
+
"""Monkeypatch Django's host validator to recognize subnet patterns."""
|
|
72
|
+
|
|
73
|
+
original_validate = http_request.validate_host
|
|
74
|
+
|
|
75
|
+
def _patched(host, allowed_hosts):
|
|
76
|
+
return validate_host_with_subnets(host, allowed_hosts, original_validate)
|
|
77
|
+
|
|
78
|
+
http_request.validate_host = _patched
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_secret_key(
|
|
82
|
+
base_dir: Path,
|
|
83
|
+
env: Mapping[str, str] | MutableMapping[str, str] | None = None,
|
|
84
|
+
secret_file: Path | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Load the Django secret key from the environment or a persisted file."""
|
|
87
|
+
|
|
88
|
+
if env is None:
|
|
89
|
+
env = os.environ
|
|
90
|
+
|
|
91
|
+
for env_var in ("DJANGO_SECRET_KEY", "SECRET_KEY"):
|
|
92
|
+
value = env.get(env_var)
|
|
93
|
+
if value:
|
|
94
|
+
return value
|
|
95
|
+
|
|
96
|
+
if secret_file is None:
|
|
97
|
+
secret_file = base_dir / "locks" / "django-secret.key"
|
|
98
|
+
|
|
99
|
+
with contextlib.suppress(OSError):
|
|
100
|
+
stored_key = secret_file.read_text(encoding="utf-8").strip()
|
|
101
|
+
if stored_key:
|
|
102
|
+
return stored_key
|
|
103
|
+
|
|
104
|
+
generated_key = get_random_secret_key()
|
|
105
|
+
with contextlib.suppress(OSError):
|
|
106
|
+
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
secret_file.write_text(generated_key, encoding="utf-8")
|
|
108
|
+
|
|
109
|
+
return generated_key
|