mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-03-16 23:03:39 +00:00
Compare commits
26 Commits
2.20.0rc5
...
feature/ro
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c10b06e3c | |||
| a04b6c3b9c | |||
| 2e9837b70d | |||
| ac4a8fb2cd | |||
| 1db8eb0ac4 | |||
| dc394f7df9 | |||
| 058f784b05 | |||
| f688768ca7 | |||
| a09ad7617d | |||
| 81aeb56ba3 | |||
| 2cd4ef5e86 | |||
| 998ed48dde | |||
| 021d88dc4c | |||
| 9012ee7144 | |||
| 945ddb2942 | |||
| 9cd0926588 | |||
| f5c12d03ec | |||
| 606ee2fb0e | |||
| 875e9cf58a | |||
| a809a4b67f | |||
| 437ae2c16c | |||
| 10661f3127 | |||
| 88ac3d82a0 | |||
| 386765ab4e | |||
| 78998182b8 | |||
| 2563391aaf |
77
docs/_static/architecture.dot
vendored
77
docs/_static/architecture.dot
vendored
@@ -138,11 +138,12 @@ digraph G {
|
|||||||
ahriman_core_http [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp",shape="box"];
|
ahriman_core_http [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp",shape="box"];
|
||||||
ahriman_core_http_sync_ahriman_client [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp\.\nsync_ahriman_client",shape="box"];
|
ahriman_core_http_sync_ahriman_client [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nhttp\.\nsync_ahriman_client",shape="box"];
|
||||||
ahriman_core_http_sync_http_client [fillcolor="#933f24",fontcolor="#ffffff",label="ahriman\.\ncore\.\nhttp\.\nsync_http_client"];
|
ahriman_core_http_sync_http_client [fillcolor="#933f24",fontcolor="#ffffff",label="ahriman\.\ncore\.\nhttp\.\nsync_http_client"];
|
||||||
ahriman_core_log [fillcolor="#e53b05",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog"];
|
ahriman_core_log [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nlog",shape="box"];
|
||||||
ahriman_core_log_http_log_handler [fillcolor="#914730",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nhttp_log_handler"];
|
ahriman_core_log_http_log_handler [fillcolor="#914730",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nhttp_log_handler"];
|
||||||
ahriman_core_log_journal_handler [fillcolor="#ac6149",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\njournal_handler"];
|
ahriman_core_log_journal_handler [fillcolor="#ac6149",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\njournal_handler"];
|
||||||
ahriman_core_log_lazy_logging [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlazy_logging"];
|
ahriman_core_log_lazy_logging [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nlog\.\nlazy_logging",shape="box"];
|
||||||
ahriman_core_log_log_loader [fillcolor="#82402b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_loader"];
|
ahriman_core_log_log_context [fillcolor="#db582f",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_context"];
|
||||||
|
ahriman_core_log_log_loader [fillcolor="#7a3c28",fontcolor="#ffffff",label="ahriman\.\ncore\.\nlog\.\nlog_loader"];
|
||||||
ahriman_core_module_loader [fillcolor="#ce5e3b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nmodule_loader"];
|
ahriman_core_module_loader [fillcolor="#ce5e3b",fontcolor="#ffffff",label="ahriman\.\ncore\.\nmodule_loader"];
|
||||||
ahriman_core_report [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport",shape="box"];
|
ahriman_core_report [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport",shape="box"];
|
||||||
ahriman_core_report_console [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport\.\nconsole",shape="box"];
|
ahriman_core_report_console [fillcolor="blue",fontcolor="white",label="ahriman\.\ncore\.\nreport\.\nconsole",shape="box"];
|
||||||
@@ -242,10 +243,11 @@ digraph G {
|
|||||||
ahriman_web_apispec_info [fillcolor="#a14f35",fontcolor="#ffffff",label="ahriman\.\nweb\.\napispec\.\ninfo"];
|
ahriman_web_apispec_info [fillcolor="#a14f35",fontcolor="#ffffff",label="ahriman\.\nweb\.\napispec\.\ninfo"];
|
||||||
ahriman_web_cors [fillcolor="#b0573a",fontcolor="#ffffff",label="ahriman\.\nweb\.\ncors"];
|
ahriman_web_cors [fillcolor="#b0573a",fontcolor="#ffffff",label="ahriman\.\nweb\.\ncors"];
|
||||||
ahriman_web_keys [fillcolor="#823017",fontcolor="#ffffff",label="ahriman\.\nweb\.\nkeys"];
|
ahriman_web_keys [fillcolor="#823017",fontcolor="#ffffff",label="ahriman\.\nweb\.\nkeys"];
|
||||||
ahriman_web_middlewares [fillcolor="#e9410c",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares"];
|
ahriman_web_middlewares [fillcolor="#ef3e06",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares"];
|
||||||
ahriman_web_middlewares_auth_handler [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nauth_handler"];
|
ahriman_web_middlewares_auth_handler [fillcolor="#733826",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nauth_handler"];
|
||||||
ahriman_web_middlewares_exception_handler [fillcolor="#994b33",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nexception_handler"];
|
ahriman_web_middlewares_exception_handler [fillcolor="#994b33",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nexception_handler"];
|
||||||
ahriman_web_middlewares_metrics_handler [fillcolor="#a34628",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nmetrics_handler"];
|
ahriman_web_middlewares_metrics_handler [fillcolor="#a34628",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nmetrics_handler"];
|
||||||
|
ahriman_web_middlewares_request_id_handler [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nmiddlewares\.\nrequest_id_handler"];
|
||||||
ahriman_web_routes [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nroutes"];
|
ahriman_web_routes [fillcolor="#8a442e",fontcolor="#ffffff",label="ahriman\.\nweb\.\nroutes"];
|
||||||
ahriman_web_schemas [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas",shape="box"];
|
ahriman_web_schemas [fillcolor="blue",fontcolor="white",label="ahriman\.\nweb\.\nschemas",shape="box"];
|
||||||
ahriman_web_schemas_any_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nany_schema"];
|
ahriman_web_schemas_any_schema [fillcolor="#b85a3d",fontcolor="#ffffff",label="ahriman\.\nweb\.\nschemas\.\nany_schema"];
|
||||||
@@ -493,6 +495,7 @@ digraph G {
|
|||||||
ahriman_core -> ahriman_models_worker [fillcolor="#ef3e06",minlen="2"];
|
ahriman_core -> ahriman_models_worker [fillcolor="#ef3e06",minlen="2"];
|
||||||
ahriman_core -> ahriman_web_keys [fillcolor="#ef3e06",minlen="2"];
|
ahriman_core -> ahriman_web_keys [fillcolor="#ef3e06",minlen="2"];
|
||||||
ahriman_core -> ahriman_web_middlewares_auth_handler [fillcolor="#ef3e06",minlen="3"];
|
ahriman_core -> ahriman_web_middlewares_auth_handler [fillcolor="#ef3e06",minlen="3"];
|
||||||
|
ahriman_core -> ahriman_web_middlewares_request_id_handler [fillcolor="#ef3e06",minlen="3"];
|
||||||
ahriman_core -> ahriman_web_routes [fillcolor="#ef3e06",minlen="2"];
|
ahriman_core -> ahriman_web_routes [fillcolor="#ef3e06",minlen="2"];
|
||||||
ahriman_core -> ahriman_web_server_info [fillcolor="#ef3e06",minlen="2"];
|
ahriman_core -> ahriman_web_server_info [fillcolor="#ef3e06",minlen="2"];
|
||||||
ahriman_core -> ahriman_web_views_api_docs [fillcolor="#ef3e06",minlen="3"];
|
ahriman_core -> ahriman_web_views_api_docs [fillcolor="#ef3e06",minlen="3"];
|
||||||
@@ -848,35 +851,39 @@ digraph G {
|
|||||||
ahriman_core_http_sync_ahriman_client -> ahriman_core_http [fillcolor="blue",weight="3"];
|
ahriman_core_http_sync_ahriman_client -> ahriman_core_http [fillcolor="blue",weight="3"];
|
||||||
ahriman_core_http_sync_http_client -> ahriman_core_http [fillcolor="#933f24",weight="3"];
|
ahriman_core_http_sync_http_client -> ahriman_core_http [fillcolor="#933f24",weight="3"];
|
||||||
ahriman_core_http_sync_http_client -> ahriman_core_http_sync_ahriman_client [fillcolor="#933f24",weight="3"];
|
ahriman_core_http_sync_http_client -> ahriman_core_http_sync_ahriman_client [fillcolor="#933f24",weight="3"];
|
||||||
ahriman_core_log -> ahriman_application_application_application_properties [fillcolor="#e53b05",minlen="3"];
|
ahriman_core_log -> ahriman_application_application_application_properties [fillcolor="blue",minlen="3"];
|
||||||
ahriman_core_log -> ahriman_application_application_workers_updater [fillcolor="#e53b05",minlen="3"];
|
ahriman_core_log -> ahriman_application_application_workers_updater [fillcolor="blue",minlen="3"];
|
||||||
ahriman_core_log -> ahriman_application_handlers_handler [fillcolor="#e53b05",minlen="3"];
|
ahriman_core_log -> ahriman_application_handlers_handler [fillcolor="blue",minlen="3"];
|
||||||
ahriman_core_log -> ahriman_application_lock [fillcolor="#e53b05",minlen="2"];
|
ahriman_core_log -> ahriman_application_lock [fillcolor="blue",minlen="2"];
|
||||||
ahriman_core_log -> ahriman_core_alpm_pacman [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_alpm_pacman [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_alpm_repo [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_alpm_repo [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_archive_archive_tree [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_archive_archive_tree [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_auth_auth [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_auth_auth [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_build_tools_package_version [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_build_tools_package_version [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_build_tools_sources [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_build_tools_sources [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_build_tools_task [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_build_tools_task [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_database_migrations [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_database_migrations [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_database_operations_operations [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_database_operations_operations [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_distributed_workers_cache [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_distributed_workers_cache [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_gitremote_remote_pull [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_gitremote_remote_pull [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_gitremote_remote_push [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_gitremote_remote_push [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_http_sync_http_client [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_http_sync_http_client [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_report_report [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_report_report [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_repository_repository_properties [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_repository_repository_properties [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_spawn [fillcolor="#e53b05",weight="2"];
|
ahriman_core_log -> ahriman_core_spawn [fillcolor="blue",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_status_watcher [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_status_watcher [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_triggers_trigger [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_triggers_trigger [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_triggers_trigger_loader [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_triggers_trigger_loader [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_core_upload_upload [fillcolor="#e53b05",minlen="2",weight="2"];
|
ahriman_core_log -> ahriman_core_upload_upload [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_core_log -> ahriman_models_package [fillcolor="#e53b05",minlen="2"];
|
ahriman_core_log -> ahriman_models_package [fillcolor="blue",minlen="2"];
|
||||||
ahriman_core_log -> ahriman_models_repository_paths [fillcolor="#e53b05",minlen="2"];
|
ahriman_core_log -> ahriman_models_repository_paths [fillcolor="blue",minlen="2"];
|
||||||
|
ahriman_core_log -> ahriman_web_middlewares_request_id_handler [fillcolor="blue",minlen="3"];
|
||||||
ahriman_core_log_http_log_handler -> ahriman_core_log_log_loader [fillcolor="#914730",weight="3"];
|
ahriman_core_log_http_log_handler -> ahriman_core_log_log_loader [fillcolor="#914730",weight="3"];
|
||||||
ahriman_core_log_lazy_logging -> ahriman_core_log [fillcolor="#b85a3d",weight="3"];
|
ahriman_core_log_lazy_logging -> ahriman_core_log [fillcolor="blue",weight="3"];
|
||||||
ahriman_core_log_log_loader -> ahriman_application_handlers_handler [fillcolor="#82402b",minlen="3"];
|
ahriman_core_log_log_context -> ahriman_core_log_lazy_logging [fillcolor="#db582f",weight="3"];
|
||||||
|
ahriman_core_log_log_context -> ahriman_core_log_log_loader [fillcolor="#db582f",weight="3"];
|
||||||
|
ahriman_core_log_log_context -> ahriman_web_middlewares_request_id_handler [fillcolor="#db582f",minlen="3"];
|
||||||
|
ahriman_core_log_log_loader -> ahriman_application_handlers_handler [fillcolor="#7a3c28",minlen="3"];
|
||||||
ahriman_core_module_loader -> ahriman_application_ahriman [fillcolor="#ce5e3b",minlen="2"];
|
ahriman_core_module_loader -> ahriman_application_ahriman [fillcolor="#ce5e3b",minlen="2"];
|
||||||
ahriman_core_module_loader -> ahriman_web_routes [fillcolor="#ce5e3b",minlen="2"];
|
ahriman_core_module_loader -> ahriman_web_routes [fillcolor="#ce5e3b",minlen="2"];
|
||||||
ahriman_core_report_console -> ahriman_core_report_report [fillcolor="blue",weight="3"];
|
ahriman_core_report_console -> ahriman_core_report_report [fillcolor="blue",weight="3"];
|
||||||
@@ -1779,12 +1786,13 @@ digraph G {
|
|||||||
ahriman_web_keys -> ahriman_web_apispec_info [fillcolor="#823017",minlen="2",weight="2"];
|
ahriman_web_keys -> ahriman_web_apispec_info [fillcolor="#823017",minlen="2",weight="2"];
|
||||||
ahriman_web_keys -> ahriman_web_views_base [fillcolor="#823017",minlen="2",weight="2"];
|
ahriman_web_keys -> ahriman_web_views_base [fillcolor="#823017",minlen="2",weight="2"];
|
||||||
ahriman_web_keys -> ahriman_web_web [fillcolor="#823017",weight="2"];
|
ahriman_web_keys -> ahriman_web_web [fillcolor="#823017",weight="2"];
|
||||||
ahriman_web_middlewares -> ahriman_web_views_v1_status_metrics [fillcolor="#e9410c",minlen="2",weight="2"];
|
ahriman_web_middlewares -> ahriman_web_views_v1_status_metrics [fillcolor="#ef3e06",minlen="2",weight="2"];
|
||||||
ahriman_web_middlewares -> ahriman_web_web [fillcolor="#e9410c",weight="2"];
|
ahriman_web_middlewares -> ahriman_web_web [fillcolor="#ef3e06",weight="2"];
|
||||||
ahriman_web_middlewares_auth_handler -> ahriman_web_web [fillcolor="#733826",minlen="2",weight="2"];
|
ahriman_web_middlewares_auth_handler -> ahriman_web_web [fillcolor="#733826",minlen="2",weight="2"];
|
||||||
ahriman_web_middlewares_exception_handler -> ahriman_web_web [fillcolor="#994b33",minlen="2",weight="2"];
|
ahriman_web_middlewares_exception_handler -> ahriman_web_web [fillcolor="#994b33",minlen="2",weight="2"];
|
||||||
ahriman_web_middlewares_metrics_handler -> ahriman_web_views_v1_status_metrics [fillcolor="#a34628",minlen="2",weight="2"];
|
ahriman_web_middlewares_metrics_handler -> ahriman_web_views_v1_status_metrics [fillcolor="#a34628",minlen="2",weight="2"];
|
||||||
ahriman_web_middlewares_metrics_handler -> ahriman_web_web [fillcolor="#a34628",minlen="2",weight="2"];
|
ahriman_web_middlewares_metrics_handler -> ahriman_web_web [fillcolor="#a34628",minlen="2",weight="2"];
|
||||||
|
ahriman_web_middlewares_request_id_handler -> ahriman_web_web [fillcolor="#8a442e",minlen="2",weight="2"];
|
||||||
ahriman_web_routes -> ahriman_web_web [fillcolor="#8a442e",weight="2"];
|
ahriman_web_routes -> ahriman_web_web [fillcolor="#8a442e",weight="2"];
|
||||||
ahriman_web_schemas -> ahriman_web_apispec_decorators [fillcolor="blue",minlen="2",weight="2"];
|
ahriman_web_schemas -> ahriman_web_apispec_decorators [fillcolor="blue",minlen="2",weight="2"];
|
||||||
ahriman_web_schemas -> ahriman_web_views_v1_auditlog_events [fillcolor="blue",minlen="2",weight="2"];
|
ahriman_web_schemas -> ahriman_web_views_v1_auditlog_events [fillcolor="blue",minlen="2",weight="2"];
|
||||||
@@ -1939,6 +1947,7 @@ digraph G {
|
|||||||
aiohttp -> ahriman_web_middlewares_auth_handler [fillcolor="#f9b506",minlen="3"];
|
aiohttp -> ahriman_web_middlewares_auth_handler [fillcolor="#f9b506",minlen="3"];
|
||||||
aiohttp -> ahriman_web_middlewares_exception_handler [fillcolor="#f9b506",minlen="3"];
|
aiohttp -> ahriman_web_middlewares_exception_handler [fillcolor="#f9b506",minlen="3"];
|
||||||
aiohttp -> ahriman_web_middlewares_metrics_handler [fillcolor="#f9b506",minlen="3"];
|
aiohttp -> ahriman_web_middlewares_metrics_handler [fillcolor="#f9b506",minlen="3"];
|
||||||
|
aiohttp -> ahriman_web_middlewares_request_id_handler [fillcolor="#f9b506",minlen="3"];
|
||||||
aiohttp -> ahriman_web_routes [fillcolor="#f9b506",minlen="2"];
|
aiohttp -> ahriman_web_routes [fillcolor="#f9b506",minlen="2"];
|
||||||
aiohttp -> ahriman_web_views_api_swagger [fillcolor="#f9b506",minlen="3"];
|
aiohttp -> ahriman_web_views_api_swagger [fillcolor="#f9b506",minlen="3"];
|
||||||
aiohttp -> ahriman_web_views_base [fillcolor="#f9b506",minlen="3"];
|
aiohttp -> ahriman_web_views_base [fillcolor="#f9b506",minlen="3"];
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ ahriman.application.handlers.add module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.archives module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.backup module
|
ahriman.application.handlers.backup module
|
||||||
------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
@@ -76,6 +84,14 @@ ahriman.application.handlers.help module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.hold module
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.key\_import module
|
ahriman.application.handlers.key\_import module
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
@@ -92,6 +108,14 @@ ahriman.application.handlers.patch module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.handlers.pkgbuild module
|
||||||
|
--------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.handlers.pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.handlers.rebuild module
|
ahriman.application.handlers.rebuild module
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.alpm.pacman\_handle module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.alpm.pacman_handle
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.alpm.pkgbuild\_parser module
|
ahriman.core.alpm.pkgbuild\_parser module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ ahriman.core.database.migrations.m016\_archive module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m017\_pkgbuild module
|
||||||
|
------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m017_pkgbuild
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.database.migrations.m018\_package\_hold module
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.database.migrations.m018_package_hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.formatters.pkgbuild\_printer module
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.formatters.pkgbuild_printer
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.printer module
|
ahriman.core.formatters.printer module
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ ahriman.core.log.lazy\_logging module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.core.log.log\_context module
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.core.log.log_context
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.log.log\_loader module
|
ahriman.core.log.log\_loader module
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ ahriman.web.middlewares.metrics\_handler module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.middlewares.request\_id\_handler module
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.middlewares.request_id_handler
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
Module contents
|
Module contents
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ ahriman.web.schemas.file\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.schemas.hold\_schema module
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.schemas.hold_schema
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.info\_schema module
|
ahriman.web.schemas.info\_schema module
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ ahriman.web.views.v1.packages package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.archives module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.archives
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.changes module
|
ahriman.web.views.v1.packages.changes module
|
||||||
--------------------------------------------
|
--------------------------------------------
|
||||||
|
|
||||||
@@ -20,6 +28,14 @@ ahriman.web.views.v1.packages.dependencies module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.web.views.v1.packages.hold module
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.web.views.v1.packages.hold
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.views.v1.packages.logs module
|
ahriman.web.views.v1.packages.logs module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ Packages have strict rules of importing:
|
|||||||
* ``ahriman.application`` package must not be used outside of this package.
|
* ``ahriman.application`` package must not be used outside of this package.
|
||||||
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment.
|
* ``ahriman.core`` and ``ahriman.models`` packages don't have any import restriction. Actually we would like to totally restrict importing of ``core`` package from ``models``, but it is impossible at the moment.
|
||||||
* ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods).
|
* ``ahriman.web`` package is allowed to be imported from ``ahriman.application`` (web handler only, only ``ahriman.web.web`` methods).
|
||||||
* The idea remains the same for all imports, if an package requires some specific dependencies, it must be imported locally to keep dependencies optional.
|
|
||||||
|
The idea remains the same for all imports, if a package requires some specific dependencies, it must be imported locally to keep dependencies optional.
|
||||||
|
|
||||||
Full dependency diagram:
|
Full dependency diagram:
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ This package contains everything required for the most of application actions an
|
|||||||
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
|
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
|
||||||
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
|
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
|
||||||
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
|
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
|
||||||
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers.
|
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger, log context for injecting context variables into log records and some wrappers.
|
||||||
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
|
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
|
||||||
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
|
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
|
||||||
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
|
* ``ahriman.core.sign`` package provides sign feature (only gpg calls are available).
|
||||||
@@ -85,6 +86,7 @@ Application run
|
|||||||
#. Call ``Handler.execute`` method.
|
#. Call ``Handler.execute`` method.
|
||||||
#. Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or continue in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether the application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Config``, which utilizes stdout to print messages).
|
#. Define list of architectures to run. In case if there is more than one architecture specified run several subprocesses or continue in current process otherwise. Class attribute ``ALLOW_MULTI_ARCHITECTURE_RUN`` controls whether the application can be run in multiple processes or not - this feature is required for some handlers (e.g. ``Config``, which utilizes stdout to print messages).
|
||||||
#. In each child process call lock functions.
|
#. In each child process call lock functions.
|
||||||
|
#. Load configuration and install logging.
|
||||||
#. After success checks pass control to ``Handler.run`` method defined by specific handler class.
|
#. After success checks pass control to ``Handler.run`` method defined by specific handler class.
|
||||||
#. Return result (success or failure) of each subprocess and exit from application.
|
#. Return result (success or failure) of each subprocess and exit from application.
|
||||||
#. Some handlers may override their status and throw ``ExitCode`` exception. This exception is just silently suppressed and changes application exit code to ``1``.
|
#. Some handlers may override their status and throw ``ExitCode`` exception. This exception is just silently suppressed and changes application exit code to ``1``.
|
||||||
@@ -159,12 +161,12 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
|
|||||||
├── aur.files -> aur.files.tar.gz
|
├── aur.files -> aur.files.tar.gz
|
||||||
└── aur.files.tar.gz
|
└── aur.files.tar.gz
|
||||||
|
|
||||||
There are multiple subdirectories, some of them are commons for any repository, but some of them are not.
|
There are multiple subdirectories, some of them are common for any repository, but some of them are not.
|
||||||
|
|
||||||
* ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories:
|
* ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories:
|
||||||
|
|
||||||
* ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures.
|
* ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures.
|
||||||
* ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree.
|
* ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree. These directories only appear if ``ahriman.core.archive.ArchiveTrigger`` is enabled.
|
||||||
|
|
||||||
The archive also allows the build process to skip rebuilding a package if a matching version already exists.
|
The archive also allows the build process to skip rebuilding a package if a matching version already exists.
|
||||||
|
|
||||||
@@ -239,9 +241,9 @@ Check outdated packages
|
|||||||
There are few ways for packages to be marked as out-of-date and hence requiring rebuild. Those are following:
|
There are few ways for packages to be marked as out-of-date and hence requiring rebuild. Those are following:
|
||||||
|
|
||||||
#. User requested update of the package. It can be caused by calling ``package-add`` subcommand (or ``package-update`` with arguments).
|
#. User requested update of the package. It can be caused by calling ``package-add`` subcommand (or ``package-update`` with arguments).
|
||||||
#. The most common way for packages to be marked as out-of-dated is that the version in AUR (or the official repositories) is newer than in the repository.
|
#. The most common way for packages to be marked as out-of-date is that the version in AUR (or the official repositories) is newer than in the repository.
|
||||||
#. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one.
|
#. In addition to the above, if package is named as VCS (e.g. has suffix ``-git``) and the last update was more than specified threshold ago, the service will also try to fetch sources and check if the revision is newer than the built one.
|
||||||
#. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-dated as well.
|
#. In addition, there is ability to check if the dependencies of the package have been updated (e.g. if linked library has been renamed or the modules directory - e.g. in case of python and ruby packages - has been changed). And if so, the package will be marked as out-of-date as well.
|
||||||
|
|
||||||
Update packages
|
Update packages
|
||||||
^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^
|
||||||
@@ -255,6 +257,7 @@ This feature is divided into the following stages: check AUR for updates and run
|
|||||||
|
|
||||||
#. Download package data from AUR.
|
#. Download package data from AUR.
|
||||||
#. Bump ``pkgrel`` if there is duplicate version in the local repository (see explanation below).
|
#. Bump ``pkgrel`` if there is duplicate version in the local repository (see explanation below).
|
||||||
|
#. Check if there is already built package of the same version in archive (cross-repository support). If so, then just copy built archives and skip steps below.
|
||||||
#. Build every package in clean chroot.
|
#. Build every package in clean chroot.
|
||||||
#. Sign packages if required.
|
#. Sign packages if required.
|
||||||
#. Add packages to database and sign database if required.
|
#. Add packages to database and sign database if required.
|
||||||
@@ -313,7 +316,7 @@ Having the initial dependencies tree, the application is looking for packages wh
|
|||||||
|
|
||||||
Those paths are also filtered by regular expressions set in the configuration.
|
Those paths are also filtered by regular expressions set in the configuration.
|
||||||
|
|
||||||
All those implicit dependencies are stored in the database and extracted on each check. In case if any of the repository packages doesn't contain any entry anymore (e.g. so version has been changed or modules directory has been changed), the dependent package will be marked as out-of-dated.
|
All those implicit dependencies are stored in the database and extracted on each check. In case if any of the repository packages doesn't contain any entry anymore (e.g. so version has been changed or modules directory has been changed), the dependent package will be marked as out-of-date.
|
||||||
|
|
||||||
Core functions reference
|
Core functions reference
|
||||||
------------------------
|
------------------------
|
||||||
@@ -344,6 +347,8 @@ The ``_Context`` class itself mimics default collection interface (as is ``Mappi
|
|||||||
|
|
||||||
In order to provide statically typed interface, the ``ahriman.models.context_key.ContextKey`` class is used for both ``_Content.get`` and ``_Content.set`` methods; the context instance itself, however, does not store information about types.
|
In order to provide statically typed interface, the ``ahriman.models.context_key.ContextKey`` class is used for both ``_Content.get`` and ``_Content.set`` methods; the context instance itself, however, does not store information about types.
|
||||||
|
|
||||||
|
Logging module has its own context variables, which are required to be registered in advance to avoid possible race conditions.
|
||||||
|
|
||||||
Submodules
|
Submodules
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
||||||
@@ -370,7 +375,7 @@ Passwords must be stored in database as ``hash(password + salt)``, where ``passw
|
|||||||
|
|
||||||
means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``.
|
means that there is user ``username`` with ``read`` access and password ``password`` hashed by ``sha512`` with salt ``salt``.
|
||||||
|
|
||||||
OAuth provider uses library definitions (``aioauth-client``) in order *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
|
OAuth provider uses library definitions (``aioauth-client``) in order to *authenticate* users. It still requires user permission to be set in database, thus it inherits mapping provider without any changes. Whereas we could override ``check_credentials`` (authentication method) by something custom, OAuth flow is a bit more complex than just forward request, thus we have to implement the flow in login form.
|
||||||
|
|
||||||
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature can be used by service reporting (aka robots).
|
OAuth's implementation also allows authenticating users via username + password (in the same way as mapping does) though it is not recommended for end-users and password must be left blank. In particular this feature can be used by service reporting (aka robots).
|
||||||
|
|
||||||
@@ -383,7 +388,7 @@ Triggers
|
|||||||
|
|
||||||
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit.
|
Triggers are extensions which can be used in order to perform any actions on application start, after the update process and, finally, before the application exit.
|
||||||
|
|
||||||
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join remain part by ``.`` and interpret as module path, import module and extract attribute from it.
|
The main idea is to load classes by their full path (e.g. ``ahriman.core.upload.UploadTrigger``) by using ``importlib``: get the last part of the import and treat it as class name, join the remaining part by ``.`` and interpret as module path, import module and extract attribute from it.
|
||||||
|
|
||||||
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``list[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
|
The loaded triggers will be called with ``ahriman.models.result.Result`` and ``list[Packages]`` arguments, which describes the process result and current repository packages respectively. Any exception raised will be suppressed and will generate an exception message in logs.
|
||||||
|
|
||||||
@@ -407,7 +412,7 @@ PKGBUILD parsing
|
|||||||
|
|
||||||
The application provides a house-made shell parser ``ahriman.core.alpm.pkgbuild_parser.PkgbuildParser`` to process PKGBUILDs and extract package data from them. It relies on the ``shlex.shlex`` parser with some configuration tweaks and adds some token post-processing.
|
The application provides a house-made shell parser ``ahriman.core.alpm.pkgbuild_parser.PkgbuildParser`` to process PKGBUILDs and extract package data from them. It relies on the ``shlex.shlex`` parser with some configuration tweaks and adds some token post-processing.
|
||||||
|
|
||||||
#. During the parser process, firstly, it extract next token from the source file (basically, the word) and tries to match it to the variable assignment. If so, then just processes accordingly.
|
#. During the parser process, firstly, it extracts the next token from the source file (basically, the word) and tries to match it to the variable assignment. If so, then just processes accordingly.
|
||||||
#. If it is not an assignment, the parser checks if the token was quoted.
|
#. If it is not an assignment, the parser checks if the token was quoted.
|
||||||
#. If it wasn't quoted then the parser tries to match the array starts (two consecutive tokens like ``array=`` and ``(``) or it is function (``function``, ``()`` and ``{``).
|
#. If it wasn't quoted then the parser tries to match the array starts (two consecutive tokens like ``array=`` and ``(``) or it is function (``function``, ``()`` and ``{``).
|
||||||
#. The arrays are processed until the next closing bracket ``)``. After extraction, the parser tries to expand an array according to bash rules (``prefix{first,second}suffix`` constructions).
|
#. The arrays are processed until the next closing bracket ``)``. After extraction, the parser tries to expand an array according to bash rules (``prefix{first,second}suffix`` constructions).
|
||||||
@@ -420,6 +425,15 @@ The PKGBUILD class also provides some additional functions on top of that:
|
|||||||
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
* Ability to extract fields defined inside ``package*()`` functions, which are in particular used for the multi-packages.
|
||||||
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
* Shell substitution, which supports constructions ``$var`` (including ``${var}``), ``${var#(#)pattern}``, ``${var%(%)pattern}`` and ``${var/(/)pattern/replacement}`` (including ``#pattern`` and ``%pattern``).
|
||||||
|
|
||||||
|
HTTP client
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
The ``ahriman.core.http`` package provides a HTTP client built on top of the ``requests`` library.
|
||||||
|
|
||||||
|
The base class ``ahriman.core.http.SyncHttpClient`` wraps ``requests.Session`` and provides common features for all HTTP interactions: configurable timeouts, retry policies with exponential backoff (using ``urllib3.util.retry.Retry``), basic authentication, custom User-Agent header, error processing, and ``make_request`` method. The session is lazily created (via ``cached_property``).
|
||||||
|
|
||||||
|
On top of that, ``ahriman.core.http.SyncAhrimanClient`` extends the base client for communication with the ahriman web service specifically. It adds automatic login on session creation (using configured credentials), ``X-Request-ID`` header injection and Unix socket transport support (via ``requests-unixsocket2``) if required.
|
||||||
|
|
||||||
Additional features
|
Additional features
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@@ -445,7 +459,7 @@ Web application requires the following python packages to be installed:
|
|||||||
Middlewares
|
Middlewares
|
||||||
^^^^^^^^^^^
|
^^^^^^^^^^^
|
||||||
|
|
||||||
Service provides some custom middlewares, e.g. logging every exception (except for user ones) and user authorization.
|
Service provides some custom middlewares, e.g. logging every exception (except for user ones), user authorization and request tracing via ``X-Request-ID`` header.
|
||||||
|
|
||||||
HEAD and OPTIONS requests
|
HEAD and OPTIONS requests
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -457,7 +471,7 @@ On the other side, ``OPTIONS`` method is implemented in the ``ahriman.web.middle
|
|||||||
Web views
|
Web views
|
||||||
^^^^^^^^^
|
^^^^^^^^^
|
||||||
|
|
||||||
All web views are defined in separated package and derived from ``ahriman.web.views.base.Base`` class which provides typed interfaces for web application.
|
All web views are defined in a separate package and derived from ``ahriman.web.views.base.Base`` class which provides typed interfaces for web application.
|
||||||
|
|
||||||
REST API supports only JSON data.
|
REST API supports only JSON data.
|
||||||
|
|
||||||
@@ -476,7 +490,7 @@ The views are also divided by supporting API versions (e.g. ``v1``, ``v2``).
|
|||||||
Templating
|
Templating
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
||||||
Package provides base jinja templates which can be overridden by settings. Vanilla templates actively use bootstrap library.
|
Package provides base jinja templates which can be overridden by settings. The default web interface is a React application. The classic bootstrap-based template is still available as ``build-status-classic.jinja2`` and can be enabled via the ``web.template`` configuration option.
|
||||||
|
|
||||||
Requests and scopes
|
Requests and scopes
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|||||||
@@ -27,21 +27,26 @@ export default tseslint.config(
|
|||||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||||
|
|
||||||
// imports
|
// imports
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error",
|
"simple-import-sort/exports": "error",
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
|
||||||
// brackets
|
// core
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"@stylistic/brace-style": ["error", "1tbs"],
|
"eqeqeq": "error",
|
||||||
|
"no-console": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
|
||||||
// stylistic
|
// stylistic
|
||||||
"@stylistic/array-bracket-spacing": ["error", "never"],
|
"@stylistic/array-bracket-spacing": ["error", "never"],
|
||||||
"@stylistic/arrow-parens": ["error", "as-needed"],
|
"@stylistic/arrow-parens": ["error", "as-needed"],
|
||||||
|
"@stylistic/brace-style": ["error", "1tbs"],
|
||||||
"@stylistic/comma-dangle": ["error", "always-multiline"],
|
"@stylistic/comma-dangle": ["error", "always-multiline"],
|
||||||
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
|
"@stylistic/comma-spacing": ["error", { before: false, after: true }],
|
||||||
"@stylistic/eol-last": ["error", "always"],
|
"@stylistic/eol-last": ["error", "always"],
|
||||||
"@stylistic/indent": ["error", 4],
|
"@stylistic/indent": ["error", 4],
|
||||||
|
"@stylistic/jsx-curly-brace-presence": ["error", { props: "never", children: "never" }],
|
||||||
"@stylistic/jsx-quotes": ["error", "prefer-double"],
|
"@stylistic/jsx-quotes": ["error", "prefer-double"],
|
||||||
|
"@stylistic/jsx-self-closing-comp": ["error", { component: true, html: true }],
|
||||||
"@stylistic/max-len": ["error", {
|
"@stylistic/max-len": ["error", {
|
||||||
code: 120,
|
code: 120,
|
||||||
ignoreComments: true,
|
ignoreComments: true,
|
||||||
@@ -49,6 +54,7 @@ export default tseslint.config(
|
|||||||
ignoreTemplateLiterals: true,
|
ignoreTemplateLiterals: true,
|
||||||
ignoreUrls: true,
|
ignoreUrls: true,
|
||||||
}],
|
}],
|
||||||
|
"@stylistic/member-delimiter-style": ["error", { multiline: { delimiter: "semi" }, singleline: { delimiter: "semi" } }],
|
||||||
"@stylistic/no-extra-parens": ["error", "all"],
|
"@stylistic/no-extra-parens": ["error", "all"],
|
||||||
"@stylistic/no-multi-spaces": "error",
|
"@stylistic/no-multi-spaces": "error",
|
||||||
"@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
|
"@stylistic/no-multiple-empty-lines": ["error", { max: 1 }],
|
||||||
@@ -58,10 +64,14 @@ export default tseslint.config(
|
|||||||
"@stylistic/semi": ["error", "always"],
|
"@stylistic/semi": ["error", "always"],
|
||||||
|
|
||||||
// typescript
|
// typescript
|
||||||
|
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
|
||||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
||||||
"@typescript-eslint/no-deprecated": "error",
|
"@typescript-eslint/no-deprecated": "error",
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "error",
|
||||||
|
"@typescript-eslint/prefer-optional-chain": "error",
|
||||||
|
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "ahriman-frontend",
|
"name": "ahriman-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.20.0-rc4",
|
"version": "2.20.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -11,32 +11,31 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@mui/icons-material": "^7.3.8",
|
"@mui/icons-material": "^7.3.9",
|
||||||
"@mui/material": "^7.3.8",
|
"@mui/material": "^7.3.9",
|
||||||
"@mui/x-data-grid": "^8.27.3",
|
"@mui/x-data-grid": "^8.27.4",
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.3.1",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-syntax-highlighter": "^16.1.1"
|
"react-syntax-highlighter": "^16.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.3",
|
"@eslint/js": "^9.39.3",
|
||||||
"@stylistic/eslint-plugin": "^5.9.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"eslint": "^9.39.3",
|
"eslint": "^9.39.3",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.56.1",
|
"typescript-eslint": "^8.56.1",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.0"
|
||||||
"vite-tsconfig-paths": "^6.1.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,14 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import CssBaseline from "@mui/material/CssBaseline";
|
|
||||||
import { ThemeProvider } from "@mui/material/styles";
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import AppLayout from "components/layout/AppLayout";
|
import AppLayout from "components/layout/AppLayout";
|
||||||
import { AuthProvider } from "contexts/AuthProvider";
|
import { AuthProvider } from "contexts/AuthProvider";
|
||||||
import { ClientProvider } from "contexts/ClientProvider";
|
import { ClientProvider } from "contexts/ClientProvider";
|
||||||
import { NotificationProvider } from "contexts/NotificationProvider";
|
import { NotificationProvider } from "contexts/NotificationProvider";
|
||||||
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
import { RepositoryProvider } from "contexts/RepositoryProvider";
|
||||||
|
import { ThemeProvider } from "contexts/ThemeProvider";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import Theme from "theme/Theme";
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -39,8 +37,7 @@ const queryClient = new QueryClient({
|
|||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
return <QueryClientProvider client={queryClient}>
|
return <QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={Theme}>
|
<ThemeProvider>
|
||||||
<CssBaseline />
|
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<ClientProvider>
|
<ClientProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class Client {
|
|||||||
|
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
|
"X-Request-ID": crypto.randomUUID?.() ?? Date.now().toString(),
|
||||||
};
|
};
|
||||||
if (json !== undefined) {
|
if (json !== undefined) {
|
||||||
headers["Content-Type"] = "application/json";
|
headers["Content-Type"] = "application/json";
|
||||||
@@ -49,7 +50,7 @@ export class Client {
|
|||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
const requestInit: RequestInit = {
|
const requestInit: RequestInit = {
|
||||||
method: method || (json ? "POST" : "GET"),
|
method: method ?? (json ? "POST" : "GET"),
|
||||||
headers,
|
headers,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ export class ServiceClient {
|
|||||||
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
return this.client.request("/api/v1/service/pgp", { method: "POST", json: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async servicePackageHoldUpdate(packageBase: string, repository: RepositoryId, isHeld: boolean): Promise<void> {
|
||||||
|
return this.client.request(`/api/v1/packages/${encodeURIComponent(packageBase)}/hold`, {
|
||||||
|
method: "POST",
|
||||||
|
query: repository.toQuery(),
|
||||||
|
json: { is_held: isHeld },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
async serviceRebuild(repository: RepositoryId, packages: string[]): Promise<void> {
|
||||||
return this.client.request("/api/v1/service/rebuild", {
|
return this.client.request("/api/v1/service/rebuild", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import { blue } from "@mui/material/colors";
|
||||||
import type { Event } from "models/Event";
|
import type { Event } from "models/Event";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
@@ -33,6 +34,8 @@ export default function EventDurationLineChart({ events }: EventDurationLineChar
|
|||||||
{
|
{
|
||||||
label: "update duration, s",
|
label: "update duration, s",
|
||||||
data: updateEvents.map(event => event.data?.took ?? 0),
|
data: updateEvents.map(event => event.data?.took ?? 0),
|
||||||
|
borderColor: blue[500],
|
||||||
|
backgroundColor: blue[200],
|
||||||
cubicInterpolationMode: "monotone" as const,
|
cubicInterpolationMode: "monotone" as const,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ export default function PackageCountBarChart({ stats }: PackageCountBarChartProp
|
|||||||
data={{
|
data={{
|
||||||
labels: ["packages"],
|
labels: ["packages"],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
|
||||||
label: "archives",
|
|
||||||
data: [stats.packages ?? 0],
|
|
||||||
backgroundColor: blue[500],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "bases",
|
label: "bases",
|
||||||
data: [stats.bases ?? 0],
|
data: [stats.bases ?? 0],
|
||||||
backgroundColor: indigo[300],
|
backgroundColor: indigo[300],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "archives",
|
||||||
|
data: [stats.packages ?? 0],
|
||||||
|
backgroundColor: blue[500],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
options={{
|
options={{
|
||||||
@@ -48,7 +48,7 @@ export default function PackageCountBarChart({ stats }: PackageCountBarChartProp
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
scales: {
|
scales: {
|
||||||
x: { stacked: true },
|
x: { stacked: true },
|
||||||
y: { stacked: true },
|
y: { stacked: false },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>;
|
/>;
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export default function AutoRefreshControl({
|
|||||||
<Tooltip title="Auto-refresh">
|
<Tooltip title="Auto-refresh">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label="Auto-refresh"
|
||||||
onClick={event => setAnchorEl(event.currentTarget)}
|
onClick={event => setAnchorEl(event.currentTarget)}
|
||||||
color={enabled ? "primary" : "default"}
|
color={enabled ? "primary" : "default"}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,47 +17,57 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import "components/common/syntaxLanguages";
|
||||||
|
|
||||||
|
import { Box, useTheme } from "@mui/material";
|
||||||
import CopyButton from "components/common/CopyButton";
|
import CopyButton from "components/common/CopyButton";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import React, { type RefObject } from "react";
|
import React, { type RefObject } from "react";
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { githubGist, vs2015 } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
||||||
|
|
||||||
interface CodeBlockProps {
|
interface CodeBlockProps {
|
||||||
preRef?: RefObject<HTMLElement | null>;
|
content: string;
|
||||||
getText: () => string;
|
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
|
language?: string;
|
||||||
onScroll?: () => void;
|
onScroll?: () => void;
|
||||||
wordBreak?: boolean;
|
preRef?: RefObject<HTMLElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CodeBlock({
|
export default function CodeBlock({
|
||||||
preRef,
|
content,
|
||||||
getText,
|
|
||||||
height,
|
height,
|
||||||
|
language = "text",
|
||||||
onScroll,
|
onScroll,
|
||||||
wordBreak,
|
preRef,
|
||||||
}: CodeBlockProps): React.JSX.Element {
|
}: CodeBlockProps): React.JSX.Element {
|
||||||
|
const { mode } = useThemeMode();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
return <Box sx={{ position: "relative" }}>
|
return <Box sx={{ position: "relative" }}>
|
||||||
<Box
|
<Box
|
||||||
ref={preRef}
|
ref={preRef}
|
||||||
component="pre"
|
|
||||||
onScroll={onScroll}
|
onScroll={onScroll}
|
||||||
sx={{
|
sx={{ overflow: "auto", height }}
|
||||||
backgroundColor: "grey.100",
|
>
|
||||||
p: 2,
|
<SyntaxHighlighter
|
||||||
borderRadius: 1,
|
language={language}
|
||||||
overflow: "auto",
|
style={mode === "dark" ? vs2015 : githubGist}
|
||||||
height,
|
wrapLongLines
|
||||||
|
customStyle={{
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
borderRadius: `${theme.shape.borderRadius}px`,
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
...wordBreak ? { whiteSpace: "pre-wrap", wordBreak: "break-all" } : {},
|
margin: 0,
|
||||||
|
minHeight: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<code>
|
{content}
|
||||||
{getText()}
|
</SyntaxHighlighter>
|
||||||
</code>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
|
||||||
<CopyButton getText={getText} />
|
|
||||||
</Box>
|
</Box>
|
||||||
|
{content && <Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
|
<CopyButton text={content} />
|
||||||
|
</Box>}
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ import { IconButton, Tooltip } from "@mui/material";
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface CopyButtonProps {
|
interface CopyButtonProps {
|
||||||
getText: () => string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CopyButton({ getText }: CopyButtonProps): React.JSX.Element {
|
export default function CopyButton({ text }: CopyButtonProps): React.JSX.Element {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const timer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
useEffect(() => () => clearTimeout(timer.current), []);
|
useEffect(() => () => clearTimeout(timer.current), []);
|
||||||
|
|
||||||
const handleCopy: () => Promise<void> = async () => {
|
const handleCopy: () => Promise<void> = async () => {
|
||||||
await navigator.clipboard.writeText(getText());
|
await navigator.clipboard.writeText(text);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
clearTimeout(timer.current);
|
clearTimeout(timer.current);
|
||||||
timer.current = setTimeout(() => setCopied(false), 2000);
|
timer.current = setTimeout(() => setCopied(false), 2000);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ import type React from "react";
|
|||||||
export default function RepositorySelect({
|
export default function RepositorySelect({
|
||||||
repositorySelect,
|
repositorySelect,
|
||||||
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
}: { repositorySelect: SelectedRepositoryResult }): React.JSX.Element {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
|
|
||||||
return <FormControl fullWidth margin="normal">
|
return <FormControl fullWidth margin="normal">
|
||||||
<InputLabel>repository</InputLabel>
|
<InputLabel>repository</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
value={repositorySelect.selectedKey || (current?.key ?? "")}
|
value={repositorySelect.selectedKey || (currentRepository?.key ?? "")}
|
||||||
label="repository"
|
label="repository"
|
||||||
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
onChange={event => repositorySelect.setSelectedKey(event.target.value)}
|
||||||
>
|
>
|
||||||
|
|||||||
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
27
frontend/src/components/common/syntaxLanguages.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash";
|
||||||
|
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
||||||
|
import plaintext from "react-syntax-highlighter/dist/esm/languages/hljs/plaintext";
|
||||||
|
|
||||||
|
SyntaxHighlighter.registerLanguage("bash", bash);
|
||||||
|
SyntaxHighlighter.registerLanguage("diff", diff);
|
||||||
|
SyntaxHighlighter.registerLanguage("text", plaintext);
|
||||||
@@ -36,11 +36,11 @@ interface DashboardDialogProps {
|
|||||||
|
|
||||||
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
export default function DashboardDialog({ open, onClose }: DashboardDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
|
|
||||||
const { data: status } = useQuery<InternalStatus>({
|
const { data: status } = useQuery<InternalStatus>({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,12 +86,12 @@ export default function DashboardDialog({ open, onClose }: DashboardDialogProps)
|
|||||||
|
|
||||||
<Grid container spacing={2} sx={{ mt: 2 }}>
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Box sx={{ maxHeight: 300 }}>
|
<Box sx={{ height: 300 }}>
|
||||||
<PackageCountBarChart stats={status.stats} />
|
<PackageCountBarChart stats={status.stats} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 12, md: 6 }}>
|
<Grid size={{ xs: 12, md: 6 }}>
|
||||||
<Box sx={{ maxHeight: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
<Box sx={{ height: 300, display: "flex", justifyContent: "center", alignItems: "center" }}>
|
||||||
<StatusPieChart counters={status.packages} />
|
<StatusPieChart counters={status.packages} />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function KeyImportDialog({ open, onClose }: KeyImportDialogProps)
|
|||||||
/>
|
/>
|
||||||
{keyBody &&
|
{keyBody &&
|
||||||
<Box sx={{ mt: 2 }}>
|
<Box sx={{ mt: 2 }}>
|
||||||
<CodeBlock getText={() => keyBody} height={300} />
|
<CodeBlock content={keyBody} height={300} />
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import EventsTab from "components/package/EventsTab";
|
|||||||
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
import PackageDetailsGrid from "components/package/PackageDetailsGrid";
|
||||||
import PackageInfoActions from "components/package/PackageInfoActions";
|
import PackageInfoActions from "components/package/PackageInfoActions";
|
||||||
import PackagePatchesList from "components/package/PackagePatchesList";
|
import PackagePatchesList from "components/package/PackagePatchesList";
|
||||||
|
import PkgbuildTab from "components/package/PkgbuildTab";
|
||||||
|
import { type TabKey, tabs } from "components/package/TabKey";
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
import { useAutoRefresh } from "hooks/useAutoRefresh";
|
||||||
@@ -55,7 +57,7 @@ export default function PackageInfoDialog({
|
|||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
}: PackageInfoDialogProps): React.JSX.Element {
|
}: PackageInfoDialogProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -65,11 +67,11 @@ export default function PackageInfoDialog({
|
|||||||
setLocalPackageBase(packageBase);
|
setLocalPackageBase(packageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [tabIndex, setTabIndex] = useState(0);
|
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setTabIndex(0);
|
setActiveTab("logs");
|
||||||
setRefreshDatabase(true);
|
setRefreshDatabase(true);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -77,16 +79,17 @@ export default function PackageInfoDialog({
|
|||||||
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("package-info-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packageData } = useQuery<PackageStatus[]>({
|
const { data: packageData } = useQuery<PackageStatus[]>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.package(localPackageBase, current) : ["packages"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.package(localPackageBase, currentRepository) : ["packages"],
|
||||||
queryFn: localPackageBase && current ? () => client.fetch.fetchPackage(localPackageBase, current) : skipToken,
|
queryFn: localPackageBase && currentRepository ?
|
||||||
|
() => client.fetch.fetchPackage(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: dependencies } = useQuery<Dependencies>({
|
const { data: dependencies } = useQuery<Dependencies>({
|
||||||
queryKey: localPackageBase && current ? QueryKeys.dependencies(localPackageBase, current) : ["dependencies"],
|
queryKey: localPackageBase && currentRepository ? QueryKeys.dependencies(localPackageBase, currentRepository) : ["dependencies"],
|
||||||
queryFn: localPackageBase && current
|
queryFn: localPackageBase && currentRepository ?
|
||||||
? () => client.fetch.fetchPackageDependencies(localPackageBase, current) : skipToken,
|
() => client.fetch.fetchPackageDependencies(localPackageBase, currentRepository) : skipToken,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,11 +105,12 @@ export default function PackageInfoDialog({
|
|||||||
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
const headerStyle = status ? StatusHeaderStyles[status.status] : {};
|
||||||
|
|
||||||
const handleUpdate: () => Promise<void> = async () => {
|
const handleUpdate: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageAdd(current, { packages: [localPackageBase], refresh: refreshDatabase });
|
await client.service.servicePackageAdd(
|
||||||
|
currentRepository, { packages: [localPackageBase], refresh: refreshDatabase });
|
||||||
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
showSuccess("Success", `Run update for packages ${localPackageBase}`);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `Package update failed: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -114,11 +118,11 @@ export default function PackageInfoDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove: () => Promise<void> = async () => {
|
const handleRemove: () => Promise<void> = async () => {
|
||||||
if (!localPackageBase || !current) {
|
if (!localPackageBase || !currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.service.servicePackageRemove(current, [localPackageBase]);
|
await client.service.servicePackageRemove(currentRepository, [localPackageBase]);
|
||||||
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
showSuccess("Success", `Packages ${localPackageBase} have been removed`);
|
||||||
onClose();
|
onClose();
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
@@ -126,6 +130,19 @@ export default function PackageInfoDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleHoldToggle: () => Promise<void> = async () => {
|
||||||
|
if (!localPackageBase || !currentRepository) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newHeldStatus = !(status?.is_held ?? false);
|
||||||
|
await client.service.servicePackageHoldUpdate(localPackageBase, currentRepository, newHeldStatus);
|
||||||
|
void queryClient.invalidateQueries({ queryKey: QueryKeys.package(localPackageBase, currentRepository) });
|
||||||
|
} catch (exception) {
|
||||||
|
showError("Action failed", `Could not update hold status: ${ApiError.errorDetail(exception)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
const handleDeletePatch: (key: string) => Promise<void> = async key => {
|
||||||
if (!localPackageBase) {
|
if (!localPackageBase) {
|
||||||
return;
|
return;
|
||||||
@@ -156,25 +173,26 @@ export default function PackageInfoDialog({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
<Tabs value={tabIndex} onChange={(_, index: number) => setTabIndex(index)}>
|
<Tabs value={activeTab} onChange={(_, tab: TabKey) => setActiveTab(tab)}>
|
||||||
<Tab label="Build logs" />
|
{tabs.map(({ key, label }) => <Tab key={key} value={key} label={label} />)}
|
||||||
<Tab label="Changes" />
|
|
||||||
<Tab label="Events" />
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{tabIndex === 0 && localPackageBase && current &&
|
{activeTab === "logs" && localPackageBase && currentRepository &&
|
||||||
<BuildLogsTab
|
<BuildLogsTab
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={current}
|
repository={currentRepository}
|
||||||
refreshInterval={autoRefresh.interval}
|
refreshInterval={autoRefresh.interval}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{tabIndex === 1 && localPackageBase && current &&
|
{activeTab === "changes" && localPackageBase && currentRepository &&
|
||||||
<ChangesTab packageBase={localPackageBase} repository={current} />
|
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{tabIndex === 2 && localPackageBase && current &&
|
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
||||||
<EventsTab packageBase={localPackageBase} repository={current} />
|
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
|
}
|
||||||
|
{activeTab === "events" && localPackageBase && currentRepository &&
|
||||||
|
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -184,6 +202,8 @@ export default function PackageInfoDialog({
|
|||||||
isAuthorized={isAuthorized}
|
isAuthorized={isAuthorized}
|
||||||
refreshDatabase={refreshDatabase}
|
refreshDatabase={refreshDatabase}
|
||||||
onRefreshDatabaseChange={setRefreshDatabase}
|
onRefreshDatabaseChange={setRefreshDatabase}
|
||||||
|
isHeld={status?.is_held ?? false}
|
||||||
|
onHoldToggle={() => void handleHoldToggle()}
|
||||||
onUpdate={() => void handleUpdate()}
|
onUpdate={() => void handleUpdate()}
|
||||||
onRemove={() => void handleRemove()}
|
onRemove={() => void handleRemove()}
|
||||||
autoRefreshIntervals={autoRefreshIntervals}
|
autoRefreshIntervals={autoRefreshIntervals}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box, Container } from "@mui/material";
|
import Brightness4Icon from "@mui/icons-material/Brightness4";
|
||||||
|
import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||||
|
import { Box, Container, IconButton, Tooltip } from "@mui/material";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import LoginDialog from "components/dialogs/LoginDialog";
|
import LoginDialog from "components/dialogs/LoginDialog";
|
||||||
import Footer from "components/layout/Footer";
|
import Footer from "components/layout/Footer";
|
||||||
@@ -27,6 +29,7 @@ import { QueryKeys } from "hooks/QueryKeys";
|
|||||||
import { useAuth } from "hooks/useAuth";
|
import { useAuth } from "hooks/useAuth";
|
||||||
import { useClient } from "hooks/useClient";
|
import { useClient } from "hooks/useClient";
|
||||||
import { useRepository } from "hooks/useRepository";
|
import { useRepository } from "hooks/useRepository";
|
||||||
|
import { useThemeMode } from "hooks/useThemeMode";
|
||||||
import type { InfoResponse } from "models/InfoResponse";
|
import type { InfoResponse } from "models/InfoResponse";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { setAuthState } = useAuth();
|
const { setAuthState } = useAuth();
|
||||||
const { setRepositories } = useRepository();
|
const { setRepositories } = useRepository();
|
||||||
|
const { mode, toggleTheme } = useThemeMode();
|
||||||
const [loginOpen, setLoginOpen] = useState(false);
|
const [loginOpen, setLoginOpen] = useState(false);
|
||||||
|
|
||||||
const { data: info } = useQuery<InfoResponse>({
|
const { data: info } = useQuery<InfoResponse>({
|
||||||
@@ -52,12 +56,17 @@ export default function AppLayout(): React.JSX.Element {
|
|||||||
|
|
||||||
return <Container maxWidth="xl">
|
return <Container maxWidth="xl">
|
||||||
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", py: 1, gap: 1 }}>
|
||||||
<a href="https://github.com/arcan1s/ahriman" title="logo">
|
<a href="https://ahriman.readthedocs.io/" title="logo">
|
||||||
<img src="/static/logo.svg" width={30} height={30} alt="" />
|
<img src="/static/logo.svg" width={30} height={30} alt="" />
|
||||||
</a>
|
</a>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
</Box>
|
</Box>
|
||||||
|
<Tooltip title="Toggle theme">
|
||||||
|
<IconButton aria-label="Toggle theme" onClick={toggleTheme}>
|
||||||
|
{mode === "dark" ? <Brightness7Icon /> : <Brightness4Icon />}
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<PackageTable
|
<PackageTable
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ import { useRepository } from "hooks/useRepository";
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
export default function Navbar(): React.JSX.Element | null {
|
export default function Navbar(): React.JSX.Element | null {
|
||||||
const { repositories, current, setCurrent } = useRepository();
|
const { repositories, currentRepository, setCurrentRepository } = useRepository();
|
||||||
|
|
||||||
if (repositories.length === 0 || !current) {
|
if (repositories.length === 0 || !currentRepository) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = repositories.findIndex(repository =>
|
const currentIndex = repositories.findIndex(repository =>
|
||||||
repository.architecture === current.architecture && repository.repository === current.repository,
|
repository.architecture === currentRepository.architecture &&
|
||||||
|
repository.repository === currentRepository.repository,
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
return <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
@@ -38,7 +39,7 @@ export default function Navbar(): React.JSX.Element | null {
|
|||||||
onChange={(_, newValue: number) => {
|
onChange={(_, newValue: number) => {
|
||||||
const repository = repositories[newValue];
|
const repository = repositories[newValue];
|
||||||
if (repository) {
|
if (repository) {
|
||||||
setCurrent(repository);
|
setCurrentRepository(repository);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
|
|||||||
@@ -175,10 +175,9 @@ export default function BuildLogsTab({
|
|||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
preRef={preRef}
|
preRef={preRef}
|
||||||
getText={() => displayedLogs}
|
content={displayedLogs}
|
||||||
height={400}
|
height={400}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
wordBreak
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|||||||
@@ -17,19 +17,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { Box } from "@mui/material";
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
import CopyButton from "components/common/CopyButton";
|
|
||||||
import { QueryKeys } from "hooks/QueryKeys";
|
|
||||||
import { useClient } from "hooks/useClient";
|
|
||||||
import type { Changes } from "models/Changes";
|
|
||||||
import type { RepositoryId } from "models/RepositoryId";
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
|
|
||||||
import diff from "react-syntax-highlighter/dist/esm/languages/hljs/diff";
|
|
||||||
import { githubGist } from "react-syntax-highlighter/dist/esm/styles/hljs";
|
|
||||||
|
|
||||||
SyntaxHighlighter.registerLanguage("diff", diff);
|
|
||||||
|
|
||||||
interface ChangesTabProps {
|
interface ChangesTabProps {
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
@@ -37,34 +28,7 @@ interface ChangesTabProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
export default function ChangesTab({ packageBase, repository }: ChangesTabProps): React.JSX.Element {
|
||||||
const client = useClient();
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
const { data } = useQuery<Changes>({
|
return <CodeBlock language="diff" content={data?.changes ?? ""} height={400} />;
|
||||||
queryKey: QueryKeys.changes(packageBase, repository),
|
|
||||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
|
||||||
enabled: !!packageBase,
|
|
||||||
});
|
|
||||||
|
|
||||||
const changesText = data?.changes ?? "";
|
|
||||||
|
|
||||||
return <Box sx={{ position: "relative", mt: 1 }}>
|
|
||||||
<SyntaxHighlighter
|
|
||||||
language="diff"
|
|
||||||
style={githubGist}
|
|
||||||
customStyle={{
|
|
||||||
padding: "16px",
|
|
||||||
borderRadius: "4px",
|
|
||||||
overflow: "auto",
|
|
||||||
height: 400,
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontFamily: "monospace",
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{changesText}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
|
||||||
<CopyButton getText={() => changesText} />
|
|
||||||
</Box>
|
|
||||||
</Box>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
|
||||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||||
|
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
|
||||||
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
import { Button, Checkbox, DialogActions, FormControlLabel } from "@mui/material";
|
||||||
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
import AutoRefreshControl from "components/common/AutoRefreshControl";
|
||||||
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
import type { AutoRefreshInterval } from "models/AutoRefreshInterval";
|
||||||
@@ -26,6 +28,8 @@ import type React from "react";
|
|||||||
|
|
||||||
interface PackageInfoActionsProps {
|
interface PackageInfoActionsProps {
|
||||||
isAuthorized: boolean;
|
isAuthorized: boolean;
|
||||||
|
isHeld: boolean;
|
||||||
|
onHoldToggle: () => void;
|
||||||
refreshDatabase: boolean;
|
refreshDatabase: boolean;
|
||||||
onRefreshDatabaseChange: (checked: boolean) => void;
|
onRefreshDatabaseChange: (checked: boolean) => void;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
@@ -39,6 +43,8 @@ export default function PackageInfoActions({
|
|||||||
isAuthorized,
|
isAuthorized,
|
||||||
refreshDatabase,
|
refreshDatabase,
|
||||||
onRefreshDatabaseChange,
|
onRefreshDatabaseChange,
|
||||||
|
isHeld,
|
||||||
|
onHoldToggle,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onRemove,
|
onRemove,
|
||||||
autoRefreshIntervals,
|
autoRefreshIntervals,
|
||||||
@@ -52,6 +58,9 @@ export default function PackageInfoActions({
|
|||||||
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
control={<Checkbox checked={refreshDatabase} onChange={(_, checked) => onRefreshDatabaseChange(checked)} size="small" />}
|
||||||
label="update pacman databases"
|
label="update pacman databases"
|
||||||
/>
|
/>
|
||||||
|
<Button onClick={onHoldToggle} variant="outlined" color="warning" startIcon={isHeld ? <PlayCircleIcon /> : <PauseCircleIcon />} size="small">
|
||||||
|
{isHeld ? "unhold" : "hold"}
|
||||||
|
</Button>
|
||||||
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
<Button onClick={onUpdate} variant="contained" color="success" startIcon={<PlayArrowIcon />} size="small">
|
||||||
update
|
update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function PackagePatchesList({
|
|||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
{editable &&
|
{editable &&
|
||||||
<IconButton size="small" color="error" onClick={() => onDelete(patch.key)}>
|
<IconButton size="small" color="error" aria-label="Remove patch" onClick={() => onDelete(patch.key)}>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
|
|||||||
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
34
frontend/src/components/package/PkgbuildTab.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import CodeBlock from "components/common/CodeBlock";
|
||||||
|
import { usePackageChanges } from "hooks/usePackageChanges";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface PkgbuildTabProps {
|
||||||
|
packageBase: string;
|
||||||
|
repository: RepositoryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PkgbuildTab({ packageBase, repository }: PkgbuildTabProps): React.JSX.Element {
|
||||||
|
const data = usePackageChanges(packageBase, repository);
|
||||||
|
|
||||||
|
return <CodeBlock language="bash" content={data?.pkgbuild ?? ""} height={400} />;
|
||||||
|
}
|
||||||
27
frontend/src/components/package/TabKey.ts
Normal file
27
frontend/src/components/package/TabKey.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
export type TabKey = "logs" | "changes" | "pkgbuild" | "events";
|
||||||
|
|
||||||
|
export const tabs: { key: TabKey; label: string }[] = [
|
||||||
|
{ key: "logs", label: "Build logs" },
|
||||||
|
{ key: "changes", label: "Changes" },
|
||||||
|
{ key: "pkgbuild", label: "PKGBUILD" },
|
||||||
|
{ key: "events", label: "Events" },
|
||||||
|
];
|
||||||
@@ -107,7 +107,8 @@ export default function PackageTable({ autoRefreshIntervals }: PackageTableProps
|
|||||||
width: 120,
|
width: 120,
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
renderCell: (params: GridRenderCellParams<PackageRow>) => <StatusCell status={params.row.status} />,
|
renderCell: (params: GridRenderCellParams<PackageRow>) =>
|
||||||
|
<StatusCell status={params.row.status} isHeld={params.row.isHeld} />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import PauseCircleIcon from "@mui/icons-material/PauseCircle";
|
||||||
import { Chip } from "@mui/material";
|
import { Chip } from "@mui/material";
|
||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
@@ -24,10 +25,12 @@ import { StatusColors } from "theme/StatusColors";
|
|||||||
|
|
||||||
interface StatusCellProps {
|
interface StatusCellProps {
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
isHeld?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatusCell({ status }: StatusCellProps): React.JSX.Element {
|
export default function StatusCell({ status, isHeld }: StatusCellProps): React.JSX.Element {
|
||||||
return <Chip
|
return <Chip
|
||||||
|
icon={isHeld ? <PauseCircleIcon /> : undefined}
|
||||||
label={status}
|
label={status}
|
||||||
size="small"
|
size="small"
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import { createContext } from "react";
|
|||||||
|
|
||||||
export interface RepositoryContextValue {
|
export interface RepositoryContextValue {
|
||||||
repositories: RepositoryId[];
|
repositories: RepositoryId[];
|
||||||
current: RepositoryId | null;
|
currentRepository: RepositoryId | null;
|
||||||
setRepositories: (repositories: RepositoryId[]) => void;
|
setRepositories: (repositories: RepositoryId[]) => void;
|
||||||
setCurrent: (repository: RepositoryId) => void;
|
setCurrentRepository: (repository: RepositoryId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
export const RepositoryContext = createContext<RepositoryContextValue | null>(null);
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ export function RepositoryProvider({ children }: { children: ReactNode }): React
|
|||||||
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
const [repositories, setRepositories] = useState<RepositoryId[]>([]);
|
||||||
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
const hash = useSyncExternalStore(subscribeToHash, getHashSnapshot);
|
||||||
|
|
||||||
const current = useMemo(() => {
|
const currentRepository = useMemo(() => {
|
||||||
if (repositories.length === 0) {
|
if (repositories.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
return repositories.find(repository => repository.key === hash) ?? repositories[0] ?? null;
|
||||||
}, [repositories, hash]);
|
}, [repositories, hash]);
|
||||||
|
|
||||||
const setCurrent = useCallback((repository: RepositoryId) => {
|
const setCurrentRepository = useCallback((repository: RepositoryId) => {
|
||||||
window.location.hash = repository.key;
|
window.location.hash = repository.key;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const value = useMemo(() => ({
|
||||||
repositories, current, setRepositories, setCurrent,
|
repositories, currentRepository, setRepositories, setCurrentRepository,
|
||||||
}), [repositories, current, setCurrent]);
|
}), [repositories, currentRepository, setCurrentRepository]);
|
||||||
|
|
||||||
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
return <RepositoryContext.Provider value={value}>{children}</RepositoryContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|||||||
27
frontend/src/contexts/ThemeContext.ts
Normal file
27
frontend/src/contexts/ThemeContext.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export interface ThemeContextValue {
|
||||||
|
mode: "light" | "dark";
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||||
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
56
frontend/src/contexts/ThemeProvider.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import CssBaseline from "@mui/material/CssBaseline";
|
||||||
|
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
|
||||||
|
import { defaults as chartDefaults } from "chart.js";
|
||||||
|
import { ThemeContext } from "contexts/ThemeContext";
|
||||||
|
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||||
|
import React, { useCallback, useEffect, useMemo } from "react";
|
||||||
|
import { createAppTheme } from "theme/Theme";
|
||||||
|
|
||||||
|
function systemPreference(): "light" | "dark" {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||||
|
const [mode, setMode] = useLocalStorage<"light" | "dark">("theme-mode", systemPreference());
|
||||||
|
|
||||||
|
const toggleTheme = useCallback(() => {
|
||||||
|
setMode(prev => prev === "light" ? "dark" : "light");
|
||||||
|
}, [setMode]);
|
||||||
|
|
||||||
|
const theme = useMemo(() => createAppTheme(mode), [mode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const textColor = mode === "dark" ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.7)";
|
||||||
|
const gridColor = mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
||||||
|
chartDefaults.color = textColor;
|
||||||
|
chartDefaults.borderColor = gridColor;
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ mode, toggleTheme }), [mode, toggleTheme]);
|
||||||
|
|
||||||
|
return <ThemeContext.Provider value={value}>
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</ThemeContext.Provider>;
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function usePackageActions(
|
|||||||
setSelectionModel: (model: string[]) => void,
|
setSelectionModel: (model: string[]) => void,
|
||||||
): UsePackageActionsResult {
|
): UsePackageActionsResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -50,13 +50,13 @@ export function usePackageActions(
|
|||||||
action: (repository: RepositoryId) => Promise<string>,
|
action: (repository: RepositoryId) => Promise<string>,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
if (!current) {
|
if (!currentRepository) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const successMessage = await action(current);
|
const successMessage = await action(currentRepository);
|
||||||
showSuccess("Success", successMessage);
|
showSuccess("Success", successMessage);
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
setSelectionModel([]);
|
setSelectionModel([]);
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
showError("Action failed", `${errorMessage}: ${ApiError.errorDetail(exception)}`);
|
||||||
@@ -64,8 +64,8 @@ export function usePackageActions(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleReload: () => void = () => {
|
const handleReload: () => void = () => {
|
||||||
if (current !== null) {
|
if (currentRepository !== null) {
|
||||||
invalidate(current);
|
invalidate(currentRepository);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
36
frontend/src/hooks/usePackageChanges.ts
Normal file
36
frontend/src/hooks/usePackageChanges.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { QueryKeys } from "hooks/QueryKeys";
|
||||||
|
import { useClient } from "hooks/useClient";
|
||||||
|
import type { Changes } from "models/Changes";
|
||||||
|
import type { RepositoryId } from "models/RepositoryId";
|
||||||
|
|
||||||
|
export function usePackageChanges(packageBase: string, repository: RepositoryId): Changes | undefined {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { data } = useQuery<Changes>({
|
||||||
|
queryKey: QueryKeys.changes(packageBase, repository),
|
||||||
|
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||||
|
enabled: !!packageBase,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -39,20 +39,20 @@ export interface UsePackageDataResult {
|
|||||||
|
|
||||||
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
export function usePackageData(autoRefreshIntervals: AutoRefreshInterval[]): UsePackageDataResult {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const { current } = useRepository();
|
const { currentRepository } = useRepository();
|
||||||
const { isAuthorized } = useAuth();
|
const { isAuthorized } = useAuth();
|
||||||
|
|
||||||
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
const autoRefresh = useAutoRefresh("table-autoreload-button", defaultInterval(autoRefreshIntervals));
|
||||||
|
|
||||||
const { data: packages = [], isLoading } = useQuery({
|
const { data: packages = [], isLoading } = useQuery({
|
||||||
queryKey: current ? QueryKeys.packages(current) : ["packages"],
|
queryKey: currentRepository ? QueryKeys.packages(currentRepository) : ["packages"],
|
||||||
queryFn: current ? () => client.fetch.fetchPackages(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchPackages(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: status } = useQuery({
|
const { data: status } = useQuery({
|
||||||
queryKey: current ? QueryKeys.status(current) : ["status"],
|
queryKey: currentRepository ? QueryKeys.status(currentRepository) : ["status"],
|
||||||
queryFn: current ? () => client.fetch.fetchServerStatus(current) : skipToken,
|
queryFn: currentRepository ? () => client.fetch.fetchServerStatus(currentRepository) : skipToken,
|
||||||
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
refetchInterval: autoRefresh.interval > 0 ? autoRefresh.interval : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export interface SelectedRepositoryResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectedRepository(): SelectedRepositoryResult {
|
export function useSelectedRepository(): SelectedRepositoryResult {
|
||||||
const { repositories, current } = useRepository();
|
const { repositories, currentRepository } = useRepository();
|
||||||
const [selectedKey, setSelectedKey] = useState("");
|
const [selectedKey, setSelectedKey] = useState("");
|
||||||
|
|
||||||
let selectedRepository: RepositoryId | null = current;
|
let selectedRepository: RepositoryId | null = currentRepository;
|
||||||
if (selectedKey) {
|
if (selectedKey) {
|
||||||
const repository = repositories.find(repository => repository.key === selectedKey);
|
const repository = repositories.find(repository => repository.key === selectedKey);
|
||||||
if (repository) {
|
if (repository) {
|
||||||
|
|||||||
25
frontend/src/hooks/useThemeMode.ts
Normal file
25
frontend/src/hooks/useThemeMode.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021-2026 ahriman team.
|
||||||
|
*
|
||||||
|
* This file is part of ahriman
|
||||||
|
* (see https://github.com/arcan1s/ahriman).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import { ThemeContext, type ThemeContextValue } from "contexts/ThemeContext";
|
||||||
|
import { useContextNotNull } from "hooks/useContextNotNull";
|
||||||
|
|
||||||
|
export function useThemeMode(): ThemeContextValue {
|
||||||
|
return useContextNotNull(ThemeContext);
|
||||||
|
}
|
||||||
@@ -20,4 +20,5 @@
|
|||||||
export interface Changes {
|
export interface Changes {
|
||||||
changes?: string;
|
changes?: string;
|
||||||
last_commit_sha?: string;
|
last_commit_sha?: string;
|
||||||
|
pkgbuild?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class PackageRow {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
timestampValue: number;
|
timestampValue: number;
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
|
isHeld: boolean;
|
||||||
|
|
||||||
constructor(descriptor: PackageStatus) {
|
constructor(descriptor: PackageStatus) {
|
||||||
this.id = descriptor.package.base;
|
this.id = descriptor.package.base;
|
||||||
@@ -45,6 +46,7 @@ export class PackageRow {
|
|||||||
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
this.timestamp = new Date(descriptor.status.timestamp * 1000).toISOStringShort();
|
||||||
this.timestampValue = descriptor.status.timestamp;
|
this.timestampValue = descriptor.status.timestamp;
|
||||||
this.status = descriptor.status.status;
|
this.status = descriptor.status.status;
|
||||||
|
this.isHeld = descriptor.status.is_held ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
private static extractListProperties(pkg: PackageStatus["package"], property: "groups" | "licenses"): string[] {
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ import type { BuildStatus } from "models/BuildStatus";
|
|||||||
export interface Status {
|
export interface Status {
|
||||||
status: BuildStatus;
|
status: BuildStatus;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
is_held?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ import { amber, green, grey, orange, red } from "@mui/material/colors";
|
|||||||
import type { BuildStatus } from "models/BuildStatus";
|
import type { BuildStatus } from "models/BuildStatus";
|
||||||
|
|
||||||
const base: Record<BuildStatus, string> = {
|
const base: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[900],
|
pending: amber[700],
|
||||||
building: orange[900],
|
building: orange[800],
|
||||||
failed: red[900],
|
failed: red[700],
|
||||||
success: green[800],
|
success: green[700],
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerBase: Record<BuildStatus, string> = {
|
const headerBase: Record<BuildStatus, string> = {
|
||||||
unknown: grey[800],
|
unknown: grey[600],
|
||||||
pending: amber[700],
|
pending: amber[700],
|
||||||
building: orange[600],
|
building: orange[600],
|
||||||
failed: red[500],
|
failed: red[500],
|
||||||
|
|||||||
@@ -17,17 +17,28 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import { createTheme } from "@mui/material/styles";
|
import { createTheme, type Theme } from "@mui/material/styles";
|
||||||
|
|
||||||
const Theme = createTheme({
|
export function createAppTheme(mode: "light" | "dark"): Theme {
|
||||||
|
return createTheme({
|
||||||
components: {
|
components: {
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
startIcon: {
|
||||||
|
alignItems: "center",
|
||||||
|
display: "flex",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
MuiDialog: {
|
MuiDialog: {
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
maxWidth: "lg",
|
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
|
maxWidth: "lg",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
palette: {
|
||||||
|
mode,
|
||||||
export default Theme;
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import { defineConfig, type Plugin } from "vite";
|
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { defineConfig, type Plugin } from "vite";
|
||||||
|
|
||||||
function renameHtml(newName: string): Plugin {
|
function rename(oldName: string, newName: string): Plugin {
|
||||||
return {
|
return {
|
||||||
name: "rename-html",
|
name: "rename",
|
||||||
enforce: "post",
|
enforce: "post",
|
||||||
generateBundle(_, bundle) {
|
generateBundle(_, bundle) {
|
||||||
if (bundle["index.html"]) {
|
if (bundle[oldName]) {
|
||||||
bundle["index.html"].fileName = newName;
|
bundle[oldName].fileName = newName;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tsconfigPaths(), renameHtml("build-status.jinja2")],
|
plugins: [react(), rename("index.html", "build-status.jinja2")],
|
||||||
base: "/",
|
base: "/",
|
||||||
|
resolve: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 10000,
|
chunkSizeWarningLimit: 10000,
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pkgbase='ahriman'
|
pkgbase='ahriman'
|
||||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||||
pkgver=2.20.0rc5
|
pkgver=2.20.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
@@ -17,11 +17,10 @@ source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$
|
|||||||
build() {
|
build() {
|
||||||
cd "$pkgbase-$pkgver"
|
cd "$pkgbase-$pkgver"
|
||||||
|
|
||||||
python -m build --wheel --no-isolation
|
npm --prefix "frontend" install --cache "$srcdir/npm-cache"
|
||||||
|
npm --prefix "frontend" run build
|
||||||
|
|
||||||
cd "frontend"
|
python -m build --wheel --no-isolation
|
||||||
npm install --cache "$srcdir/npm-cache"
|
|
||||||
npm run build
|
|
||||||
}
|
}
|
||||||
|
|
||||||
package_ahriman() {
|
package_ahriman() {
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ formatter = syslog_format
|
|||||||
args = ("/dev/log",)
|
args = ("/dev/log",)
|
||||||
|
|
||||||
[formatter_generic_format]
|
[formatter_generic_format]
|
||||||
format = [%(levelname)s %(asctime)s] [%(name)s]: %(message)s
|
format = [{levelname} {asctime}] [{name}]: {message}
|
||||||
|
style = {
|
||||||
|
|
||||||
[formatter_syslog_format]
|
[formatter_syslog_format]
|
||||||
format = [%(levelname)s] [%(name)s]: %(message)s
|
format = [{levelname}] [{name}]: {message}
|
||||||
|
style = {
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level = DEBUG
|
level = DEBUG
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2026\-03\-06" "ahriman 2.20.0rc5" "ArcH linux ReposItory MANager"
|
.TH AHRIMAN "1" "2026\-03\-08" "ahriman 2.20.0" "ArcH linux ReposItory MANager"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ahriman \- ArcH linux ReposItory MANager
|
ahriman \- ArcH linux ReposItory MANager
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2.20.0rc5"
|
__version__ = "2.20.0"
|
||||||
|
|||||||
@@ -154,13 +154,13 @@ class Application(ApplicationPackages, ApplicationRepository):
|
|||||||
for package_name, packager in missing.items():
|
for package_name, packager in missing.items():
|
||||||
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
|
||||||
# there is local cache, load package from it
|
# there is local cache, load package from it
|
||||||
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
|
leaf = Package.from_build(source_dir, self.repository.repository_id.architecture, packager)
|
||||||
else:
|
else:
|
||||||
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
leaf = Package.from_aur(package_name, packager, include_provides=True)
|
||||||
portion[leaf.base] = leaf
|
portion[leaf.base] = leaf
|
||||||
|
|
||||||
# register package in the database
|
# register package in the database
|
||||||
self.repository.reporter.set_unknown(leaf)
|
self.reporter.set_unknown(leaf)
|
||||||
|
|
||||||
return portion
|
return portion
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
if last_commit_sha is None:
|
if last_commit_sha is None:
|
||||||
continue # skip check in case if we can't calculate diff
|
continue # skip check in case if we can't calculate diff
|
||||||
|
|
||||||
changes = self.repository.package_changes(package, last_commit_sha)
|
if (changes := self.repository.package_changes(package, last_commit_sha)) is not None:
|
||||||
self.repository.reporter.package_changes_update(package.base, changes)
|
self.reporter.package_changes_update(package.base, changes)
|
||||||
|
|
||||||
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
def clean(self, *, cache: bool, chroot: bool, manual: bool, packages: bool, pacman: bool) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -162,6 +162,10 @@ class ApplicationRepository(ApplicationProperties):
|
|||||||
self.on_result(build_result)
|
self.on_result(build_result)
|
||||||
result.merge(build_result)
|
result.merge(build_result)
|
||||||
|
|
||||||
|
# filter packages which were prebuilt
|
||||||
|
succeeded = {package.base for package in build_result.success}
|
||||||
|
updates = filter(lambda package: package.base not in succeeded, updates)
|
||||||
|
|
||||||
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
builder = Updater.load(self.repository_id, self.configuration, self.repository)
|
||||||
|
|
||||||
# ok so for now we split all packages into chunks and process each chunk accordingly
|
# ok so for now we split all packages into chunks and process each chunk accordingly
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import argparse
|
|||||||
|
|
||||||
from ahriman.application.application import Application
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.handlers.update import Update
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import enum_values, extract_user
|
from ahriman.core.utils import enum_values, extract_user
|
||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.packagers import Packagers
|
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@@ -48,26 +48,7 @@ class Add(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Add.perform_action(application, args)
|
||||||
application.add(args.package, args.source, args.username)
|
|
||||||
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
|
||||||
for package in args.package: # for each requested package insert patch
|
|
||||||
for patch in patches:
|
|
||||||
application.reporter.package_patches_update(package, patch)
|
|
||||||
|
|
||||||
if not args.now:
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Add.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -103,14 +84,34 @@ class Add(Handler):
|
|||||||
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
|
||||||
action=argparse.BooleanOptionalAction, default=True)
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
|
||||||
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
|
||||||
"-yy to force refresh even if up to date",
|
|
||||||
action="count", default=False)
|
|
||||||
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
|
||||||
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
|
||||||
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build",
|
||||||
action="append")
|
action="append")
|
||||||
|
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
|
||||||
|
"-yy to force refresh even if up to date",
|
||||||
|
action="count", default=False)
|
||||||
|
parser.set_defaults(aur=False, check_files=False, dry_run=False, local=False, manual=True, vcs=False)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform add action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
application.add(args.package, args.source, args.username)
|
||||||
|
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
|
||||||
|
for package in args.package: # for each requested package insert patch
|
||||||
|
for patch in patches:
|
||||||
|
application.reporter.package_patches_update(package, patch)
|
||||||
|
|
||||||
|
if not args.now:
|
||||||
|
return
|
||||||
|
Update.perform_action(application, args)
|
||||||
|
|
||||||
arguments = [_set_package_add_parser]
|
arguments = [_set_package_add_parser]
|
||||||
|
|||||||
81
src/ahriman/application/handlers/archives.py
Normal file
81
src/ahriman/application/handlers/archives.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PackagePrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Archives(Handler):
|
||||||
|
"""
|
||||||
|
package archives handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=True)
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
archives = application.repository.package_archives(args.package)
|
||||||
|
for package in archives:
|
||||||
|
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
|
||||||
|
|
||||||
|
Archives.check_status(args.exit_code, bool(archives))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-archives", help="list package archive versions",
|
||||||
|
description="list available archive versions for the package")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.add_argument("--info", help="show additional package information",
|
||||||
|
action=argparse.BooleanOptionalAction, default=False)
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -47,14 +47,13 @@ class Change(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.List:
|
case Action.List:
|
||||||
changes = client.package_changes_get(args.package)
|
changes = client.package_changes_get(args.package)
|
||||||
ChangesPrinter(changes)(verbose=True, separator="")
|
ChangesPrinter(changes)(verbose=True, separator="")
|
||||||
Change.check_status(args.exit_code, not changes.is_empty)
|
Change.check_status(args.exit_code, changes.changes is not None)
|
||||||
case Action.Remove:
|
case Action.Remove:
|
||||||
client.package_changes_update(args.package, Changes())
|
client.package_changes_update(args.package, Changes())
|
||||||
|
|
||||||
|
|||||||
93
src/ahriman/application/handlers/hold.py
Normal file
93
src/ahriman/application/handlers/hold.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Hold(Handler):
|
||||||
|
"""
|
||||||
|
package hold handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.Remove:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=False)
|
||||||
|
case Action.Update:
|
||||||
|
for package in args.package:
|
||||||
|
client.package_hold_update(package, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for hold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-hold", help="hold package",
|
||||||
|
description="hold package from automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for unhold package subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-unhold", help="unhold package",
|
||||||
|
description="remove package hold, allowing automatic updates")
|
||||||
|
parser.add_argument("package", help="package base", nargs="+")
|
||||||
|
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
_set_package_hold_parser,
|
||||||
|
_set_package_unhold_parser,
|
||||||
|
]
|
||||||
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
100
src/ahriman/application/handlers/pkgbuild.py
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.formatters import PkgbuildPrinter
|
||||||
|
from ahriman.models.action import Action
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Pkgbuild(Handler):
|
||||||
|
"""
|
||||||
|
package pkgbuild handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
|
|
||||||
|
match args.action:
|
||||||
|
case Action.List:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
PkgbuildPrinter(changes)(verbose=True, separator="")
|
||||||
|
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
|
||||||
|
case Action.Remove:
|
||||||
|
changes = client.package_changes_get(args.package)
|
||||||
|
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
|
||||||
|
description="retrieve package PKGBUILD stored in database",
|
||||||
|
epilog="This command requests package status from the web interface "
|
||||||
|
"if it is available.")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||||
|
action="store_true")
|
||||||
|
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package pkgbuild remove subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
|
||||||
|
description="remove the package PKGBUILD stored remotely")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
|
||||||
@@ -44,8 +44,7 @@ class Reload(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
client.configuration_reload()
|
client.configuration_reload()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
132
src/ahriman/application/handlers/rollback.py
Normal file
132
src/ahriman/application/handlers/rollback.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ahriman.application.application import Application
|
||||||
|
from ahriman.application.handlers.add import Add
|
||||||
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
|
from ahriman.core.utils import extract_user
|
||||||
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.package_source import PackageSource
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
|
|
||||||
|
class Rollback(Handler):
|
||||||
|
"""
|
||||||
|
package rollback handler
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||||
|
report: bool) -> None:
|
||||||
|
"""
|
||||||
|
callback for command line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
report(bool): force enable or disable reporting
|
||||||
|
"""
|
||||||
|
application = Application(repository_id, configuration, report=report)
|
||||||
|
application.on_start()
|
||||||
|
|
||||||
|
package = Rollback.package_load(application, args.package, args.version)
|
||||||
|
artifacts = Rollback.package_artifacts(application, package)
|
||||||
|
|
||||||
|
args.package = [str(artifact) for artifact in artifacts]
|
||||||
|
Add.perform_action(application, args)
|
||||||
|
|
||||||
|
if args.hold:
|
||||||
|
application.reporter.package_hold_update(package.base, enabled=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
|
"""
|
||||||
|
add parser for package archives subcommand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root(SubParserAction): subparsers for the commands
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
argparse.ArgumentParser: created argument parser
|
||||||
|
"""
|
||||||
|
parser = root.add_parser("package-rollback", help="rollback package",
|
||||||
|
description="rollback package to specified version from archives")
|
||||||
|
parser.add_argument("package", help="package base")
|
||||||
|
parser.add_argument("version", help="package version")
|
||||||
|
parser.add_argument("--hold", help="hold package afterwards",
|
||||||
|
action=argparse.BooleanOptionalAction, default=True)
|
||||||
|
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
|
||||||
|
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
|
||||||
|
exit_code=False, increment=False, now=True, local=False, manual=False, refresh=False,
|
||||||
|
source=PackageSource.Archive, variable=None, vcs=False)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_artifacts(application: Application, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
look for package artifacts and returns paths to them if any
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package(Package): package descriptor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: paths to found artifacts
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if artifacts do not exist
|
||||||
|
"""
|
||||||
|
# lookup for built artifacts
|
||||||
|
artifacts = application.repository.package_archives_lookup(package)
|
||||||
|
if not artifacts:
|
||||||
|
raise UnknownPackageError(package.base) from None
|
||||||
|
return artifacts
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_load(application: Application, package_base: str, version: str) -> Package:
|
||||||
|
"""
|
||||||
|
load package from given arguments
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
package_base(str): package base
|
||||||
|
version(str): package version
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: loaded package
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UnknownPackageError: if package does not exist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
package, _ = next(iter(application.reporter.package_get(package_base)))
|
||||||
|
package.version = version
|
||||||
|
|
||||||
|
return package
|
||||||
|
except StopIteration:
|
||||||
|
raise UnknownPackageError(package_base) from None
|
||||||
|
|
||||||
|
arguments = [_set_package_archives_parser]
|
||||||
@@ -52,7 +52,7 @@ class Status(Handler):
|
|||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
# we are using reporter here
|
# we are using reporter here
|
||||||
client = Application(repository_id, configuration, report=True).repository.reporter
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
if args.ahriman:
|
if args.ahriman:
|
||||||
service_status = client.status_get()
|
service_status = client.status_get()
|
||||||
StatusPrinter(service_status.status)(verbose=args.info)
|
StatusPrinter(service_status.status)(verbose=args.info)
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ class StatusUpdate(Handler):
|
|||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
report(bool): force enable or disable reporting
|
report(bool): force enable or disable reporting
|
||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=True)
|
client = Application(repository_id, configuration, report=True).reporter
|
||||||
client = application.repository.reporter
|
|
||||||
|
|
||||||
match args.action:
|
match args.action:
|
||||||
case Action.Update if args.package:
|
case Action.Update if args.package:
|
||||||
|
|||||||
@@ -48,22 +48,7 @@ class Update(Handler):
|
|||||||
"""
|
"""
|
||||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||||
application.on_start()
|
application.on_start()
|
||||||
|
Update.perform_action(application, args)
|
||||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
|
||||||
check_files=args.check_files)
|
|
||||||
if args.changes: # generate changes if requested
|
|
||||||
application.changes(packages)
|
|
||||||
|
|
||||||
if args.dry_run: # exit from application if no build requested
|
|
||||||
Update.check_status(args.exit_code, packages) # status code check
|
|
||||||
return
|
|
||||||
|
|
||||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
|
||||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
|
||||||
|
|
||||||
application.print_updates(packages, log_fn=application.logger.info)
|
|
||||||
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
|
||||||
Update.check_status(args.exit_code, not result.is_empty)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@@ -153,6 +138,31 @@ class Update(Handler):
|
|||||||
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
return print(line) if dry_run else application.logger.info(line) # pylint: disable=bad-builtin
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def perform_action(application: Application, args: argparse.Namespace) -> None:
|
||||||
|
"""
|
||||||
|
perform update action
|
||||||
|
|
||||||
|
Args:
|
||||||
|
application(Application): application instance
|
||||||
|
args(argparse.Namespace): command line args
|
||||||
|
"""
|
||||||
|
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||||
|
check_files=args.check_files)
|
||||||
|
if args.changes: # generate changes if requested
|
||||||
|
application.changes(packages)
|
||||||
|
|
||||||
|
if args.dry_run: # exit from application if no build requested
|
||||||
|
Update.check_status(args.exit_code, packages) # status code check
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||||
|
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||||
|
|
||||||
|
application.print_updates(packages, log_fn=application.logger.info)
|
||||||
|
result = application.update(packages, packagers, bump_pkgrel=args.increment)
|
||||||
|
Update.check_status(args.exit_code, not result.is_empty)
|
||||||
|
|
||||||
arguments = [
|
arguments = [
|
||||||
_set_repo_check_parser,
|
_set_repo_check_parser,
|
||||||
_set_repo_update_parser,
|
_set_repo_update_parser,
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import tarfile
|
|||||||
from collections.abc import Iterable, Iterator
|
from collections.abc import Iterable, Iterator
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||||
|
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import trim_package
|
from ahriman.core.utils import trim_package
|
||||||
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
|
|||||||
self.refresh_database = refresh_database
|
self.refresh_database = refresh_database
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def handle(self) -> Handle:
|
def handle(self) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
pyalpm handle
|
pyalpm handle
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: generated pyalpm handle instance
|
PacmanHandle: generated pyalpm handle instance
|
||||||
"""
|
"""
|
||||||
return self.__create_handle(refresh_database=self.refresh_database)
|
return self.__create_handle(refresh_database=self.refresh_database)
|
||||||
|
|
||||||
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
|
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
|
||||||
"""
|
"""
|
||||||
create lazy handle function
|
create lazy handle function
|
||||||
|
|
||||||
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
|
|||||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Handle: fully initialized pacman handle
|
PacmanHandle: fully initialized pacman handle
|
||||||
"""
|
"""
|
||||||
pacman_root = self.configuration.getpath("alpm", "database")
|
pacman_root = self.configuration.getpath("alpm", "database")
|
||||||
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||||
|
|
||||||
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
||||||
root = self.configuration.getpath("alpm", "root")
|
root = self.configuration.getpath("alpm", "root")
|
||||||
handle = Handle(str(root), str(database_path))
|
handle = PacmanHandle(str(root), str(database_path))
|
||||||
|
|
||||||
for repository in self.configuration.getlist("alpm", "repositories"):
|
for repository in self.configuration.getlist("alpm", "repositories"):
|
||||||
database = self.database_init(handle, repository, self.repository_id.architecture)
|
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||||
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
||||||
"""
|
"""
|
||||||
copy database from the operating system root to the ahriman local home
|
copy database from the operating system root to the ahriman local home
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database copying
|
handle(PacmanHandle): pacman handle which will be used for database copying
|
||||||
database(DB): pacman database instance to be copied
|
database(DB): pacman database instance to be copied
|
||||||
pacman_root(Path): operating system pacman root
|
pacman_root(Path): operating system pacman root
|
||||||
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
||||||
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
|
|||||||
with self.repository_paths.preserve_owner():
|
with self.repository_paths.preserve_owner():
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
|
||||||
"""
|
"""
|
||||||
create database instance from pacman handler and set its properties
|
create database instance from pacman handler and set its properties
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database initializing
|
handle(PacmanHandle): pacman handle which will be used for database initializing
|
||||||
repository(str): pacman repository name (e.g. core)
|
repository(str): pacman repository name (e.g. core)
|
||||||
architecture(str): repository architecture
|
architecture(str): repository architecture
|
||||||
|
|
||||||
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
|
|||||||
|
|
||||||
return database
|
return database
|
||||||
|
|
||||||
def database_sync(self, handle: Handle, *, force: bool) -> None:
|
def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
|
||||||
"""
|
"""
|
||||||
sync local database
|
sync local database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
handle(Handle): pacman handle which will be used for database sync
|
handle(PacmanHandle): pacman handle which will be used for database sync
|
||||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
"""
|
"""
|
||||||
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
||||||
|
|||||||
@@ -138,8 +138,14 @@ class PacmanDatabase(SyncHttpClient):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PacmanError: on operation error (invalid scheme or incomplete configuration)
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
server = next(iter(self.database.servers))
|
server = next(iter(self.database.servers))
|
||||||
|
except StopIteration:
|
||||||
|
raise PacmanError("No configured servers available for database") from None
|
||||||
filename = f"{self.database.name}.files.tar.gz"
|
filename = f"{self.database.name}.files.tar.gz"
|
||||||
url = f"{server}/{filename}"
|
url = f"{server}/{filename}"
|
||||||
|
|
||||||
|
|||||||
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
81
src/ahriman/core/alpm/pacman_handle.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from pathlib import Path
|
||||||
|
from pyalpm import Handle, Package # type: ignore[import-not-found]
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from typing import Any, ClassVar, Self
|
||||||
|
|
||||||
|
|
||||||
|
class PacmanHandle:
|
||||||
|
"""
|
||||||
|
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
handle(Handle): pyalpm handle instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ephemeral: ClassVar[Self | None] = None
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments for :class:`pyalpm.Handle`
|
||||||
|
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
|
||||||
|
"""
|
||||||
|
self.handle = Handle(*args, **kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def ephemeral(cls) -> Self:
|
||||||
|
"""
|
||||||
|
create temporary instance with no access to real databases
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Self: loaded class
|
||||||
|
"""
|
||||||
|
if cls._ephemeral is None:
|
||||||
|
# handle creates alpm version file, but we don't use it
|
||||||
|
# so it is ok to just remove it
|
||||||
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
|
cls._ephemeral = cls("/", dir_name)
|
||||||
|
return cls._ephemeral
|
||||||
|
|
||||||
|
def package_load(self, path: Path) -> Package:
|
||||||
|
"""
|
||||||
|
load package from path to the archive
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path(Path): path to package archive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Package: package instance
|
||||||
|
"""
|
||||||
|
return self.handle.load_pkg(str(path))
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
"""
|
||||||
|
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item(str): property name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: attribute by its name
|
||||||
|
"""
|
||||||
|
return self.handle.__getattribute__(item)
|
||||||
@@ -58,7 +58,7 @@ class Auth(LazyLogging):
|
|||||||
Returns:
|
Returns:
|
||||||
str: login control as html code to insert
|
str: login control as html code to insert
|
||||||
"""
|
"""
|
||||||
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#login-modal" style="text-decoration: none"><i class="bi bi-box-arrow-in-right"></i> login</button>"""
|
return "<button type=\"button\" class=\"btn btn-link\" data-bs-toggle=\"modal\" data-bs-target=\"#login-modal\" style=\"text-decoration: none\"><i class=\"bi bi-box-arrow-in-right\"></i> login</button>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_external(self) -> bool:
|
def is_external(self) -> bool:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from typing import ClassVar
|
|||||||
from ahriman.core.exceptions import CalledProcessError
|
from ahriman.core.exceptions import CalledProcessError
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
from ahriman.core.utils import check_output, utcnow, walk
|
from ahriman.core.utils import check_output, utcnow, walk
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild import Pkgbuild
|
from ahriman.models.pkgbuild import Pkgbuild
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
@@ -51,24 +52,25 @@ class Sources(LazyLogging):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def changes(source_dir: Path, last_commit_sha: str | None) -> str | None:
|
def changes(source_dir: Path, last_commit_sha: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
extract changes from the last known commit if available
|
extract changes from the last known commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source_dir(Path): local path to directory with source files
|
source_dir(Path): local path to directory with source files
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str | None: changes from the last commit if available or ``None`` otherwise
|
Changes: changes from the last commit if available
|
||||||
"""
|
"""
|
||||||
if last_commit_sha is None:
|
|
||||||
return None # no previous reference found
|
|
||||||
|
|
||||||
instance = Sources()
|
instance = Sources()
|
||||||
|
|
||||||
|
diff = None
|
||||||
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
if instance.fetch_until(source_dir, commit_sha=last_commit_sha) is not None:
|
||||||
return instance.diff(source_dir, last_commit_sha)
|
diff = instance.diff(source_dir, last_commit_sha)
|
||||||
return None
|
pkgbuild = instance.read(source_dir, "HEAD", Path("PKGBUILD"))
|
||||||
|
|
||||||
|
return Changes(last_commit_sha, diff, pkgbuild)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
def extend_architectures(sources_dir: Path, architecture: str) -> list[PkgbuildPatch]:
|
||||||
@@ -413,3 +415,17 @@ class Sources(LazyLogging):
|
|||||||
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
cwd=sources_dir, input_data=patch.serialize(), logger=self.logger)
|
||||||
else:
|
else:
|
||||||
patch.write(sources_dir / "PKGBUILD")
|
patch.write(sources_dir / "PKGBUILD")
|
||||||
|
|
||||||
|
def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str:
|
||||||
|
"""
|
||||||
|
read file content from the specified commit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sources_dir(Path): local path to git repository
|
||||||
|
commit_sha(str): commit hash to read from
|
||||||
|
path(Path): path to file inside the repository
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: file content at specified commit
|
||||||
|
"""
|
||||||
|
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger)
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -61,12 +59,9 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
base = Package.from_archive(full_path, pacman)
|
base = Package.from_archive(full_path)
|
||||||
for package, description in base.packages.items():
|
for package, description in base.packages.items():
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"make_depends": description.make_depends,
|
"make_depends": description.make_depends,
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -58,12 +56,9 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
base = Package.from_archive(full_path, pacman)
|
base = Package.from_archive(full_path)
|
||||||
for package, description in base.packages.items():
|
for package, description in base.packages.items():
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"check_depends": description.check_depends,
|
"check_depends": description.check_depends,
|
||||||
|
|||||||
@@ -19,11 +19,9 @@
|
|||||||
#
|
#
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.utils import package_like
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["migrate_data", "steps"]
|
__all__ = ["migrate_data", "steps"]
|
||||||
@@ -64,12 +62,9 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu
|
|||||||
if not configuration.repository_paths.repository.is_dir():
|
if not configuration.repository_paths.repository.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
package_list = []
|
package_list = []
|
||||||
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
|
||||||
package = Package.from_archive(full_path, pacman)
|
package = Package.from_archive(full_path)
|
||||||
package_list.append({
|
package_list.append({
|
||||||
"package_base": package.base,
|
"package_base": package.base,
|
||||||
"packager": package.packager,
|
"packager": package.packager,
|
||||||
|
|||||||
@@ -20,13 +20,11 @@
|
|||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.repository import Explorer
|
from ahriman.core.repository import Explorer
|
||||||
from ahriman.core.sign.gpg import GPG
|
from ahriman.core.sign.gpg import GPG
|
||||||
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
|
||||||
from ahriman.models.repository_paths import RepositoryPaths
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
@@ -45,29 +43,27 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None:
|
|||||||
|
|
||||||
for repository_id in Explorer.repositories_extract(configuration):
|
for repository_id in Explorer.repositories_extract(configuration):
|
||||||
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
||||||
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
|
||||||
|
|
||||||
# create archive directory if required
|
# create archive directory if required
|
||||||
if not paths.archive.is_dir():
|
if not paths.archive.is_dir():
|
||||||
with paths.preserve_owner():
|
with paths.preserve_owner():
|
||||||
paths.archive.mkdir(mode=0o755, parents=True)
|
paths.archive.mkdir(mode=0o755, parents=True)
|
||||||
|
|
||||||
move_packages(paths, pacman)
|
move_packages(paths)
|
||||||
|
|
||||||
|
|
||||||
def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None:
|
def move_packages(repository_paths: RepositoryPaths) -> None:
|
||||||
"""
|
"""
|
||||||
move packages from repository to archive and create symbolic links
|
move packages from repository to archive and create symbolic links
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repository_paths(RepositoryPaths): repository paths instance
|
repository_paths(RepositoryPaths): repository paths instance
|
||||||
pacman(Pacman): alpm wrapper instance
|
|
||||||
"""
|
"""
|
||||||
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
||||||
if not archive.is_file(follow_symlinks=False):
|
if not archive.is_file(follow_symlinks=False):
|
||||||
continue # skip symbolic links if any
|
continue # skip symbolic links if any
|
||||||
|
|
||||||
package = Package.from_archive(archive, pacman)
|
package = Package.from_archive(archive)
|
||||||
artifacts = [archive]
|
artifacts = [archive]
|
||||||
# check if there are signatures for this package and append it here too
|
# check if there are signatures for this package and append it here too
|
||||||
if (signature := GPG.signature(archive)).exists():
|
if (signature := GPG.signature(archive)).exists():
|
||||||
|
|||||||
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
25
src/ahriman/core/database/migrations/m017_pkgbuild.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""alter table package_changes add column pkgbuild text""",
|
||||||
|
]
|
||||||
25
src/ahriman/core/database/migrations/m018_package_hold.py
Normal file
25
src/ahriman/core/database/migrations/m018_package_hold.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
__all__ = ["steps"]
|
||||||
|
|
||||||
|
|
||||||
|
steps = [
|
||||||
|
"""alter table package_statuses add column is_held integer not null default 0""",
|
||||||
|
]
|
||||||
@@ -45,10 +45,10 @@ class ChangesOperations(Operations):
|
|||||||
def run(connection: Connection) -> Changes:
|
def run(connection: Connection) -> Changes:
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
Changes(row["last_commit_sha"], row["changes"] or None)
|
Changes(row["last_commit_sha"], row["changes"] or None, row["pkgbuild"] or None)
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""
|
"""
|
||||||
select last_commit_sha, changes from package_changes
|
select last_commit_sha, changes, pkgbuild from package_changes
|
||||||
where package_base = :package_base and repository = :repository
|
where package_base = :package_base and repository = :repository
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
@@ -77,16 +77,17 @@ class ChangesOperations(Operations):
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
insert into package_changes
|
insert into package_changes
|
||||||
(package_base, last_commit_sha, changes, repository)
|
(package_base, last_commit_sha, changes, pkgbuild, repository)
|
||||||
values
|
values
|
||||||
(:package_base, :last_commit_sha, :changes ,:repository)
|
(:package_base, :last_commit_sha, :changes, :pkgbuild, :repository)
|
||||||
on conflict (package_base, repository) do update set
|
on conflict (package_base, repository) do update set
|
||||||
last_commit_sha = :last_commit_sha, changes = :changes
|
last_commit_sha = :last_commit_sha, changes = :changes, pkgbuild = :pkgbuild
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
"last_commit_sha": changes.last_commit_sha,
|
"last_commit_sha": changes.last_commit_sha,
|
||||||
"changes": changes.changes,
|
"changes": changes.changes,
|
||||||
|
"pkgbuild": changes.pkgbuild,
|
||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -211,13 +211,37 @@ class PackageOperations(Operations):
|
|||||||
dict[str, BuildStatus]: map of the package base to its status
|
dict[str, BuildStatus]: map of the package base to its status
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
|
row["package_base"]: BuildStatus(row["status"], row["last_updated"], is_held=bool(row["is_held"]))
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""select * from package_statuses where repository = :repository""",
|
"""select * from package_statuses where repository = :repository""",
|
||||||
{"repository": repository_id.id}
|
{"repository": repository_id.id}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, repository_id: RepositoryId | None = None, *,
|
||||||
|
enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
|
def run(connection: Connection) -> None:
|
||||||
|
connection.execute(
|
||||||
|
"""update package_statuses set is_held = :is_held
|
||||||
|
where package_base = :package_base and repository = :repository""",
|
||||||
|
{
|
||||||
|
"is_held": int(enabled),
|
||||||
|
"package_base": package_base,
|
||||||
|
"repository": repository_id.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self.with_connection(run, commit=True)
|
||||||
|
|
||||||
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
remove package from database
|
remove package from database
|
||||||
|
|||||||
@@ -116,6 +116,19 @@ class GitRemoteError(RuntimeError):
|
|||||||
RuntimeError.__init__(self, "Git remote failed")
|
RuntimeError.__init__(self, "Git remote failed")
|
||||||
|
|
||||||
|
|
||||||
|
class GPGError(RuntimeError):
|
||||||
|
"""
|
||||||
|
PGP/GPG related exception
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, details: str) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
details(str): details of the exception
|
||||||
|
"""
|
||||||
|
RuntimeError.__init__(self, f"GPG operation failed: {details}")
|
||||||
|
|
||||||
|
|
||||||
class InitializeError(RuntimeError):
|
class InitializeError(RuntimeError):
|
||||||
"""
|
"""
|
||||||
base service initialization exception
|
base service initialization exception
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from ahriman.core.formatters.event_stats_printer import EventStatsPrinter
|
|||||||
from ahriman.core.formatters.package_printer import PackagePrinter
|
from ahriman.core.formatters.package_printer import PackagePrinter
|
||||||
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
||||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||||
|
from ahriman.core.formatters.pkgbuild_printer import PkgbuildPrinter
|
||||||
from ahriman.core.formatters.printer import Printer
|
from ahriman.core.formatters.printer import Printer
|
||||||
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
||||||
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ChangesPrinter(Printer):
|
|||||||
Returns:
|
Returns:
|
||||||
list[Property]: list of content properties
|
list[Property]: list of content properties
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.changes is None:
|
||||||
return []
|
return []
|
||||||
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
return [Property("", self.changes.changes, is_required=True, indent=0)]
|
||||||
|
|
||||||
@@ -57,6 +57,6 @@ class ChangesPrinter(Printer):
|
|||||||
Returns:
|
Returns:
|
||||||
str | None: content title if it can be generated and ``None`` otherwise
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
"""
|
"""
|
||||||
if self.changes.is_empty:
|
if self.changes.changes is None:
|
||||||
return None
|
return None
|
||||||
return self.changes.last_commit_sha
|
return self.changes.last_commit_sha
|
||||||
|
|||||||
62
src/ahriman/core/formatters/pkgbuild_printer.py
Normal file
62
src/ahriman/core/formatters/pkgbuild_printer.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ahriman.core.formatters.printer import Printer
|
||||||
|
from ahriman.models.changes import Changes
|
||||||
|
from ahriman.models.property import Property
|
||||||
|
|
||||||
|
|
||||||
|
class PkgbuildPrinter(Printer):
|
||||||
|
"""
|
||||||
|
print content of the pkgbuild stored in changes
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
changes(Changes): package changes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, changes: Changes) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
changes(Changes): package changes
|
||||||
|
"""
|
||||||
|
Printer.__init__(self)
|
||||||
|
self.changes = changes
|
||||||
|
|
||||||
|
def properties(self) -> list[Property]:
|
||||||
|
"""
|
||||||
|
convert content into printable data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Property]: list of content properties
|
||||||
|
"""
|
||||||
|
if self.changes.pkgbuild is None:
|
||||||
|
return []
|
||||||
|
return [Property("", self.changes.pkgbuild, is_required=True, indent=0)]
|
||||||
|
|
||||||
|
# pylint: disable=redundant-returns-doc
|
||||||
|
def title(self) -> str | None:
|
||||||
|
"""
|
||||||
|
generate entry title from content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str | None: content title if it can be generated and ``None`` otherwise
|
||||||
|
"""
|
||||||
|
if self.changes.pkgbuild is None:
|
||||||
|
return None
|
||||||
|
return self.changes.last_commit_sha
|
||||||
@@ -17,14 +17,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from collections.abc import Callable
|
|
||||||
from functools import cmp_to_key
|
|
||||||
|
|
||||||
from ahriman.core import context
|
from ahriman.core import context
|
||||||
from ahriman.core.alpm.pacman import Pacman
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
|
from ahriman.core.repository import Repository
|
||||||
from ahriman.core.triggers import Trigger
|
from ahriman.core.triggers import Trigger
|
||||||
from ahriman.core.utils import package_like
|
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.result import Result
|
from ahriman.models.result import Result
|
||||||
@@ -78,23 +74,21 @@ class ArchiveRotationTrigger(Trigger):
|
|||||||
"""
|
"""
|
||||||
return list(cls.CONFIGURATION_SCHEMA.keys())
|
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||||
|
|
||||||
def archives_remove(self, package: Package, pacman: Pacman) -> None:
|
def archives_remove(self, package: Package, repository: Repository) -> None:
|
||||||
"""
|
"""
|
||||||
remove older versions of the specified package
|
remove older versions of the specified package
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package which has been updated to check for older versions
|
package(Package): package which has been updated to check for older versions
|
||||||
pacman(Pacman): alpm wrapper instance
|
repository(Repository): repository instance
|
||||||
"""
|
"""
|
||||||
packages: dict[tuple[str, str], Package] = {}
|
# explicit guard to skip process in case if rotation is disabled
|
||||||
# we can't use here load_archives, because it ignores versions
|
# this guard is supposed to speedup process
|
||||||
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
|
if self.keep_built_packages == 0:
|
||||||
local = Package.from_archive(full_path, pacman)
|
return
|
||||||
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
|
|
||||||
|
to_remove = repository.package_archives(package.base)
|
||||||
|
|
||||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
|
||||||
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
|
|
||||||
# 0 will implicitly be translated into [:0], meaning we keep all packages
|
|
||||||
for single in to_remove[:-self.keep_built_packages]:
|
for single in to_remove[:-self.keep_built_packages]:
|
||||||
self.logger.info("removing version %s of package %s", single.version, single.base)
|
self.logger.info("removing version %s of package %s", single.version, single.base)
|
||||||
for archive in single.packages.values():
|
for archive in single.packages.values():
|
||||||
@@ -110,7 +104,7 @@ class ArchiveRotationTrigger(Trigger):
|
|||||||
packages(list[Package]): list of all available packages
|
packages(list[Package]): list of all available packages
|
||||||
"""
|
"""
|
||||||
ctx = context.get()
|
ctx = context.get()
|
||||||
pacman = ctx.get(Pacman)
|
repository = ctx.get(Repository)
|
||||||
|
|
||||||
for package in result.success:
|
for package in result.success:
|
||||||
self.archives_remove(package, pacman)
|
self.archives_remove(package, repository)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#
|
#
|
||||||
import contextlib
|
import contextlib
|
||||||
import requests
|
import requests
|
||||||
|
import uuid
|
||||||
|
|
||||||
from requests.adapters import BaseAdapter
|
from requests.adapters import BaseAdapter
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -60,6 +61,15 @@ class SyncAhrimanClient(SyncHttpClient):
|
|||||||
|
|
||||||
return adapters
|
return adapters
|
||||||
|
|
||||||
|
def headers(self) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
additional request headers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: additional request headers defined by class
|
||||||
|
"""
|
||||||
|
return SyncHttpClient.headers(self) | {"X-Request-ID": str(uuid.uuid4())}
|
||||||
|
|
||||||
def on_session_creation(self, session: requests.Session) -> None:
|
def on_session_creation(self, session: requests.Session) -> None:
|
||||||
"""
|
"""
|
||||||
method which will be called on session creation
|
method which will be called on session creation
|
||||||
|
|||||||
@@ -144,6 +144,15 @@ class SyncHttpClient(LazyLogging):
|
|||||||
"https://": HTTPAdapter(max_retries=self.retry),
|
"https://": HTTPAdapter(max_retries=self.retry),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def headers(self) -> dict[str, str]:
|
||||||
|
"""
|
||||||
|
additional request headers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict[str, str]: additional request headers defined by class
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
|
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
|
||||||
headers: dict[str, str] | None = None,
|
headers: dict[str, str] | None = None,
|
||||||
params: list[tuple[str, str]] | None = None,
|
params: list[tuple[str, str]] | None = None,
|
||||||
@@ -178,6 +187,9 @@ class SyncHttpClient(LazyLogging):
|
|||||||
if session is None:
|
if session is None:
|
||||||
session = self.session
|
session = self.session
|
||||||
|
|
||||||
|
if additional_headers := self.headers():
|
||||||
|
headers = additional_headers | (headers or {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
|
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
|
||||||
stream=stream, auth=self.auth, timeout=self.timeout)
|
stream=stream, auth=self.auth, timeout=self.timeout)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from collections.abc import Iterator
|
|||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ahriman.core.log.log_context import LogContext
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
|
|
||||||
|
|
||||||
@@ -54,30 +55,20 @@ class LazyLogging:
|
|||||||
prefix = "" if clazz.__module__ is None else f"{clazz.__module__}."
|
prefix = "" if clazz.__module__ is None else f"{clazz.__module__}."
|
||||||
return f"{prefix}{clazz.__qualname__}"
|
return f"{prefix}{clazz.__qualname__}"
|
||||||
|
|
||||||
@staticmethod
|
@contextlib.contextmanager
|
||||||
def _package_logger_reset() -> None:
|
def in_context(self, name: str, value: Any) -> Iterator[None]:
|
||||||
"""
|
"""
|
||||||
reset package logger to empty one
|
execute function while setting log context. The context will be reset after the execution
|
||||||
"""
|
|
||||||
logging.setLogRecordFactory(logging.LogRecord)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _package_logger_set(package_base: str, version: str | None) -> None:
|
|
||||||
"""
|
|
||||||
set package base as extra info to the logger
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package_base(str): package base
|
name(str): attribute name to set on log records
|
||||||
version(str | None): package version if available
|
value(Any): current value of the context variable
|
||||||
"""
|
"""
|
||||||
current_factory = logging.getLogRecordFactory()
|
token = LogContext.set(name, value)
|
||||||
|
try:
|
||||||
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
|
yield
|
||||||
record = current_factory(*args, **kwargs)
|
finally:
|
||||||
record.package_id = LogRecordId(package_base, version or "<unknown>")
|
LogContext.reset(name, token)
|
||||||
return record
|
|
||||||
|
|
||||||
logging.setLogRecordFactory(package_record_factory)
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def in_package_context(self, package_base: str, version: str | None) -> Iterator[None]:
|
def in_package_context(self, package_base: str, version: str | None) -> Iterator[None]:
|
||||||
@@ -94,8 +85,5 @@ class LazyLogging:
|
|||||||
>>> with self.in_package_context(package.base, package.version):
|
>>> with self.in_package_context(package.base, package.version):
|
||||||
>>> build_package(package)
|
>>> build_package(package)
|
||||||
"""
|
"""
|
||||||
try:
|
with self.in_context("package_id", LogRecordId(package_base, version or "<unknown>")):
|
||||||
self._package_logger_set(package_base, version)
|
|
||||||
yield
|
yield
|
||||||
finally:
|
|
||||||
self._package_logger_reset()
|
|
||||||
|
|||||||
108
src/ahriman/core/log/log_context.py
Normal file
108
src/ahriman/core/log/log_context.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2026 ahriman team.
|
||||||
|
#
|
||||||
|
# This file is part of ahriman
|
||||||
|
# (see https://github.com/arcan1s/ahriman).
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import contextvars
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from typing import Any, ClassVar, TypeVar, cast
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
class LogContext:
|
||||||
|
"""
|
||||||
|
logging context manager which provides context variables injection into log records
|
||||||
|
"""
|
||||||
|
|
||||||
|
_context: ClassVar[dict[str, contextvars.ContextVar[Any]]] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name: str) -> T | None:
|
||||||
|
"""
|
||||||
|
get context variable if available
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): name of the context variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
T | None: context variable if available and ``None`` otherwise
|
||||||
|
"""
|
||||||
|
if (variable := cls._context.get(name)) is not None:
|
||||||
|
return cast(T | None, variable.get())
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def log_record_factory(cls, *args: Any, **kwargs: Any) -> logging.LogRecord:
|
||||||
|
"""
|
||||||
|
log record factory which injects all registered context variables into log records
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments for the log factory
|
||||||
|
**kwargs(Any): keyword arguments for the log factory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
logging.LogRecord: log record with context variables set as attributes
|
||||||
|
"""
|
||||||
|
record = logging.LogRecord(*args, **kwargs)
|
||||||
|
|
||||||
|
for name, variable in cls._context.items():
|
||||||
|
if (value := variable.get()) is not None:
|
||||||
|
setattr(record, name, value)
|
||||||
|
|
||||||
|
return record
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, name: str) -> contextvars.ContextVar[T]:
|
||||||
|
"""
|
||||||
|
(re)create context variable for log records
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): name of the context variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
contextvars.ContextVar[T]: created context variable
|
||||||
|
"""
|
||||||
|
variable = cls._context[name] = contextvars.ContextVar(name, default=None)
|
||||||
|
return variable
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset(cls, name: str, token: contextvars.Token[T]) -> None:
|
||||||
|
"""
|
||||||
|
reset context variable to its previous value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): attribute name to reset on log records
|
||||||
|
token(contextvars.Token[T]): previously registered token
|
||||||
|
"""
|
||||||
|
cls._context[name].reset(token)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set(cls, name: str, value: T) -> contextvars.Token[T]:
|
||||||
|
"""
|
||||||
|
set context variable for log records. This value will be automatically emitted with each log record
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): attribute name to set on log records
|
||||||
|
value(T): current value of the context variable
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
contextvars.Token[T]: token created with this value
|
||||||
|
"""
|
||||||
|
return cls._context[name].set(value)
|
||||||
@@ -21,10 +21,11 @@ import logging
|
|||||||
|
|
||||||
from logging.config import fileConfig
|
from logging.config import fileConfig
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import ClassVar
|
from typing import ClassVar, Literal
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.log.http_log_handler import HttpLogHandler
|
from ahriman.core.log.http_log_handler import HttpLogHandler
|
||||||
|
from ahriman.core.log.log_context import LogContext
|
||||||
from ahriman.models.log_handler import LogHandler
|
from ahriman.models.log_handler import LogHandler
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
|
||||||
@@ -36,11 +37,13 @@ class LogLoader:
|
|||||||
Attributes:
|
Attributes:
|
||||||
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
|
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
|
||||||
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
|
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
|
||||||
|
DEFAULT_LOG_STYLE(str): (class attribute) default log style (in case of fallback)
|
||||||
DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device
|
DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_LOG_FORMAT: ClassVar[str] = "[%(levelname)s %(asctime)s] [%(name)s]: %(message)s"
|
DEFAULT_LOG_FORMAT: ClassVar[str] = "[{levelname} {asctime}] [{name}]: {message}"
|
||||||
DEFAULT_LOG_LEVEL: ClassVar[int] = logging.DEBUG
|
DEFAULT_LOG_LEVEL: ClassVar[int] = logging.DEBUG
|
||||||
|
DEFAULT_LOG_STYLE: ClassVar[Literal["%", "{", "$"]] = "{"
|
||||||
DEFAULT_SYSLOG_DEVICE: ClassVar[Path] = Path("/") / "dev" / "log"
|
DEFAULT_SYSLOG_DEVICE: ClassVar[Path] = Path("/") / "dev" / "log"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -100,10 +103,22 @@ class LogLoader:
|
|||||||
fileConfig(log_configuration, disable_existing_loggers=True)
|
fileConfig(log_configuration, disable_existing_loggers=True)
|
||||||
logging.debug("using %s logger", default_handler)
|
logging.debug("using %s logger", default_handler)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.basicConfig(filename=None, format=LogLoader.DEFAULT_LOG_FORMAT, level=LogLoader.DEFAULT_LOG_LEVEL)
|
logging.basicConfig(filename=None, format=LogLoader.DEFAULT_LOG_FORMAT,
|
||||||
|
style=LogLoader.DEFAULT_LOG_STYLE, level=LogLoader.DEFAULT_LOG_LEVEL)
|
||||||
logging.exception("could not load logging from configuration, fallback to stderr")
|
logging.exception("could not load logging from configuration, fallback to stderr")
|
||||||
|
|
||||||
HttpLogHandler.load(repository_id, configuration, report=report)
|
HttpLogHandler.load(repository_id, configuration, report=report)
|
||||||
|
LogLoader.register_context()
|
||||||
|
|
||||||
if quiet:
|
if quiet:
|
||||||
logging.disable(logging.WARNING) # only print errors here
|
logging.disable(logging.WARNING) # only print errors here
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def register_context() -> None:
|
||||||
|
"""
|
||||||
|
register logging context
|
||||||
|
"""
|
||||||
|
# predefined context variables
|
||||||
|
for variable in ("package_id", "request_id"):
|
||||||
|
LogContext.register(variable)
|
||||||
|
logging.setLogRecordFactory(LogContext.log_record_factory)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
|
|||||||
from ahriman.core.build_tools.task import Task
|
from ahriman.core.build_tools.task import Task
|
||||||
from ahriman.core.repository.cleaner import Cleaner
|
from ahriman.core.repository.cleaner import Cleaner
|
||||||
from ahriman.core.repository.package_info import PackageInfo
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.utils import atomic_move, filelock, list_flatmap, package_like, safe_filename, symlink_relative
|
from ahriman.core.utils import atomic_move, filelock, safe_filename, symlink_relative
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.event import EventType
|
from ahriman.models.event import EventType
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
@@ -41,35 +41,6 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
trait for common repository update processes
|
trait for common repository update processes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _archive_lookup(self, package: Package) -> list[Path]:
|
|
||||||
"""
|
|
||||||
check if there is a rebuilt package already
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package(Package): package to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Path]: list of built packages and signatures if available, empty list otherwise
|
|
||||||
"""
|
|
||||||
archive = self.paths.archive_for(package.base)
|
|
||||||
if not archive.is_dir():
|
|
||||||
return []
|
|
||||||
|
|
||||||
for path in filter(package_like, archive.iterdir()):
|
|
||||||
# check if package version is the same
|
|
||||||
built = Package.from_archive(path, self.pacman)
|
|
||||||
if built.version != package.version:
|
|
||||||
continue
|
|
||||||
|
|
||||||
packages = built.packages.values()
|
|
||||||
# all packages must be either any or same architecture
|
|
||||||
if not all(single.architecture in ("any", self.architecture) for single in packages):
|
|
||||||
continue
|
|
||||||
|
|
||||||
return list_flatmap(packages, lambda single: archive.glob(f"{single.filename}*"))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
|
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
|
||||||
"""
|
"""
|
||||||
rename package archive removing special symbols
|
rename package archive removing special symbols
|
||||||
@@ -102,12 +73,12 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
"""
|
"""
|
||||||
self.reporter.set_building(package.base)
|
self.reporter.set_building(package.base)
|
||||||
|
|
||||||
task = Task(package, self.configuration, self.architecture, self.paths)
|
task = Task(package, self.configuration, self.repository_id.architecture, self.paths)
|
||||||
patches = self.reporter.package_patches_get(package.base, None)
|
patches = self.reporter.package_patches_get(package.base, None)
|
||||||
commit_sha = task.init(path, patches, local_version)
|
commit_sha = task.init(path, patches, local_version)
|
||||||
|
|
||||||
loaded_package = Package.from_build(path, self.architecture, None)
|
loaded_package = Package.from_build(path, self.repository_id.architecture, None)
|
||||||
if prebuilt := list(self._archive_lookup(loaded_package)):
|
if prebuilt := self.package_archives_lookup(loaded_package):
|
||||||
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
|
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
|
||||||
built = []
|
built = []
|
||||||
for artifact in prebuilt:
|
for artifact in prebuilt:
|
||||||
@@ -117,7 +88,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
else:
|
else:
|
||||||
built = task.build(path, PACKAGER=packager)
|
built = task.build(path, PACKAGER=packager)
|
||||||
|
|
||||||
package.with_packages(built, self.pacman)
|
package.with_packages(built)
|
||||||
for src in built:
|
for src in built:
|
||||||
dst = self.paths.packages / src.name
|
dst = self.paths.packages / src.name
|
||||||
atomic_move(src, dst)
|
atomic_move(src, dst)
|
||||||
@@ -204,7 +175,8 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
|
|
||||||
# update commit hash for changes keeping current diff if there is any
|
# update commit hash for changes keeping current diff if there is any
|
||||||
changes = self.reporter.package_changes_get(single.base)
|
changes = self.reporter.package_changes_get(single.base)
|
||||||
self.reporter.package_changes_update(single.base, Changes(commit_sha, changes.changes))
|
self.reporter.package_changes_update(
|
||||||
|
single.base, Changes(commit_sha, changes.changes, changes.pkgbuild))
|
||||||
|
|
||||||
# update dependencies list
|
# update dependencies list
|
||||||
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
package_archive = PackageArchive(self.paths.build_root, single, self.pacman, self.scan_paths)
|
||||||
@@ -217,7 +189,7 @@ class Executor(PackageInfo, Cleaner):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.reporter.set_failed(single.base)
|
self.reporter.set_failed(single.base)
|
||||||
result.add_failed(single)
|
result.add_failed(single)
|
||||||
self.logger.exception("%s (%s) build exception", single.base, self.architecture)
|
self.logger.exception("%s (%s) build exception", single.base, self.repository_id.architecture)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,42 @@
|
|||||||
#
|
#
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Callable, Iterable
|
||||||
|
from functools import cmp_to_key
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
from ahriman.core.alpm.pacman import Pacman
|
||||||
from ahriman.core.build_tools.package_version import PackageVersion
|
from ahriman.core.build_tools.package_version import PackageVersion
|
||||||
from ahriman.core.build_tools.sources import Sources
|
from ahriman.core.build_tools.sources import Sources
|
||||||
from ahriman.core.repository.repository_properties import RepositoryProperties
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.utils import package_like
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.status import Client
|
||||||
|
from ahriman.core.utils import list_flatmap, package_like
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
from ahriman.models.repository_id import RepositoryId
|
||||||
|
from ahriman.models.repository_paths import RepositoryPaths
|
||||||
|
|
||||||
|
|
||||||
class PackageInfo(RepositoryProperties):
|
class PackageInfo(LazyLogging):
|
||||||
"""
|
"""
|
||||||
handler for the package information
|
handler for the package information
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
configuration(Configuration): configuration instance
|
||||||
|
pacman(Pacman): alpm wrapper instance
|
||||||
|
paths(RepositoryPaths): repository paths instance
|
||||||
|
reporter(Client): build status reporter instance
|
||||||
|
repository_id(RepositoryId): repository unique identifier
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
configuration: Configuration
|
||||||
|
pacman: Pacman
|
||||||
|
paths: RepositoryPaths
|
||||||
|
reporter: Client
|
||||||
|
repository_id: RepositoryId
|
||||||
|
|
||||||
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
generate full dependencies list including transitive dependencies
|
generate full dependencies list including transitive dependencies
|
||||||
@@ -86,7 +105,7 @@ class PackageInfo(RepositoryProperties):
|
|||||||
# we are iterating over bases, not single packages
|
# we are iterating over bases, not single packages
|
||||||
for full_path in packages:
|
for full_path in packages:
|
||||||
try:
|
try:
|
||||||
local = Package.from_archive(full_path, self.pacman)
|
local = Package.from_archive(full_path)
|
||||||
if (source := sources.get(local.base)) is not None: # update source with remote
|
if (source := sources.get(local.base)) is not None: # update source with remote
|
||||||
local.remote = source
|
local.remote = source
|
||||||
|
|
||||||
@@ -102,27 +121,76 @@ class PackageInfo(RepositoryProperties):
|
|||||||
self.logger.exception("could not load package from %s", full_path)
|
self.logger.exception("could not load package from %s", full_path)
|
||||||
return list(result.values())
|
return list(result.values())
|
||||||
|
|
||||||
def package_changes(self, package: Package, last_commit_sha: str | None) -> Changes:
|
def package_archives(self, package_base: str) -> list[Package]:
|
||||||
|
"""
|
||||||
|
load list of packages known for this package base. This method unlike
|
||||||
|
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
|
||||||
|
versions available for the ``package_base``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of packages belonging to this base, sorted by version by ascension
|
||||||
|
"""
|
||||||
|
packages: dict[tuple[str, str], Package] = {}
|
||||||
|
# we can't use here load_archives, because it ignores versions
|
||||||
|
for full_path in filter(package_like, self.paths.archive_for(package_base).iterdir()):
|
||||||
|
local = Package.from_archive(full_path)
|
||||||
|
if not local.supports_architecture(self.repository_id.architecture):
|
||||||
|
continue
|
||||||
|
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
|
||||||
|
|
||||||
|
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||||
|
return sorted(packages.values(), key=cmp_to_key(comparator))
|
||||||
|
|
||||||
|
def package_archives_lookup(self, package: Package) -> list[Path]:
|
||||||
|
"""
|
||||||
|
check if there is a rebuilt package already
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package(Package): package to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Path]: list of built packages and signatures if available, empty list otherwise
|
||||||
|
"""
|
||||||
|
archive = self.paths.archive_for(package.base)
|
||||||
|
if not archive.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
for path in filter(package_like, archive.iterdir()):
|
||||||
|
# check if package version is the same
|
||||||
|
built = Package.from_archive(path)
|
||||||
|
if built.version != package.version:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# all packages must be either any or same architecture
|
||||||
|
if not built.supports_architecture(self.repository_id.architecture):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*"))
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
|
||||||
"""
|
"""
|
||||||
extract package change for the package since last commit if available
|
extract package change for the package since last commit if available
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
package(Package): package properties
|
package(Package): package properties
|
||||||
last_commit_sha(str | None): last known commit hash
|
last_commit_sha(str): last known commit hash
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Changes: changes if available
|
Changes | None: changes if available
|
||||||
"""
|
"""
|
||||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||||
dir_path = Path(dir_name)
|
dir_path = Path(dir_name)
|
||||||
patches = self.reporter.package_patches_get(package.base, None)
|
patches = self.reporter.package_patches_get(package.base, None)
|
||||||
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
||||||
|
|
||||||
changes: str | None = None
|
|
||||||
if current_commit_sha != last_commit_sha:
|
if current_commit_sha != last_commit_sha:
|
||||||
changes = Sources.changes(dir_path, last_commit_sha)
|
return Sources.changes(dir_path, last_commit_sha)
|
||||||
|
return None
|
||||||
return Changes(last_commit_sha, changes)
|
|
||||||
|
|
||||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ class RepositoryProperties(EventLogger, LazyLogging):
|
|||||||
Attributes:
|
Attributes:
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
database(SQLite): database instance
|
database(SQLite): database instance
|
||||||
ignore_list(list[str]): package bases which will be ignored during auto updates
|
|
||||||
pacman(Pacman): alpm wrapper instance
|
pacman(Pacman): alpm wrapper instance
|
||||||
paths(RepositoryPaths): repository paths instance
|
paths(RepositoryPaths): repository paths instance
|
||||||
repo(Repo): repo commands wrapper instance
|
repo(Repo): repo commands wrapper instance
|
||||||
@@ -69,35 +68,15 @@ class RepositoryProperties(EventLogger, LazyLogging):
|
|||||||
|
|
||||||
self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing
|
self.paths: RepositoryPaths = configuration.repository_paths # additional workaround for pycharm typing
|
||||||
|
|
||||||
self.ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
|
self._ignore_list = configuration.getlist("build", "ignore_packages", fallback=[])
|
||||||
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
|
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
|
||||||
self.sign = GPG(configuration)
|
self.sign = GPG(configuration)
|
||||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
self.repo = Repo(self.repository_id.name, self.paths, self.sign.repository_sign_args)
|
||||||
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
||||||
self.triggers = TriggerLoader.load(repository_id, configuration)
|
self.triggers = TriggerLoader.load(repository_id, configuration)
|
||||||
|
|
||||||
self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
self.scan_paths = ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
||||||
|
|
||||||
@property
|
|
||||||
def architecture(self) -> str:
|
|
||||||
"""
|
|
||||||
repository architecture for backward compatibility
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: repository architecture
|
|
||||||
"""
|
|
||||||
return self.repository_id.architecture
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self) -> str:
|
|
||||||
"""
|
|
||||||
repository name for backward compatibility
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: repository name
|
|
||||||
"""
|
|
||||||
return self.repository_id.name
|
|
||||||
|
|
||||||
def packager(self, packagers: Packagers, package_base: str) -> User:
|
def packager(self, packagers: Packagers, package_base: str) -> User:
|
||||||
"""
|
"""
|
||||||
extract packager from configuration having username
|
extract packager from configuration having username
|
||||||
|
|||||||
@@ -58,12 +58,17 @@ class UpdateHandler(PackageInfo, Cleaner):
|
|||||||
continue
|
continue
|
||||||
raise UnknownPackageError(package.base)
|
raise UnknownPackageError(package.base)
|
||||||
|
|
||||||
|
ignore_list = self._ignore_list + [
|
||||||
|
package.base for package, status in self.reporter.package_get(None) if status.is_held
|
||||||
|
]
|
||||||
|
|
||||||
result: list[Package] = []
|
result: list[Package] = []
|
||||||
for local in self.packages(filter_packages):
|
for local in self.packages(filter_packages):
|
||||||
with self.in_package_context(local.base, local.version):
|
with self.in_package_context(local.base, local.version):
|
||||||
if not local.remote.is_remote:
|
if not local.remote.is_remote:
|
||||||
continue # avoid checking local packages
|
continue # avoid checking local packages
|
||||||
if local.base in self.ignore_list:
|
if local.base in ignore_list:
|
||||||
|
self.logger.info("package %s is held, skip update check", local.base)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -150,7 +155,7 @@ class UpdateHandler(PackageInfo, Cleaner):
|
|||||||
)
|
)
|
||||||
|
|
||||||
Sources.fetch(cache_dir, source)
|
Sources.fetch(cache_dir, source)
|
||||||
remote = Package.from_build(cache_dir, self.architecture, None)
|
remote = Package.from_build(cache_dir, self.repository_id.architecture, None)
|
||||||
|
|
||||||
local = packages.get(remote.base)
|
local = packages.get(remote.base)
|
||||||
if local is None:
|
if local is None:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.exceptions import BuildError
|
from ahriman.core.exceptions import BuildError, GPGError
|
||||||
from ahriman.core.http import SyncHttpClient
|
from ahriman.core.http import SyncHttpClient
|
||||||
from ahriman.core.utils import check_output
|
from ahriman.core.utils import check_output
|
||||||
from ahriman.models.sign_settings import SignSettings
|
from ahriman.models.sign_settings import SignSettings
|
||||||
@@ -147,12 +147,19 @@ class GPG(SyncHttpClient):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: full PGP key fingerprint
|
str: full PGP key fingerprint
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GPGError: if key is in wrong format
|
||||||
"""
|
"""
|
||||||
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
|
||||||
# fingerprint line will be like
|
# fingerprint line will be like
|
||||||
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
|
# fpr:::::::::43A663569A07EE1E4ECC55CC7E3A4240CE3C45C2:
|
||||||
|
metadata = check_output("gpg", "--with-colons", "--fingerprint", key, logger=self.logger)
|
||||||
|
|
||||||
|
try:
|
||||||
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
|
fingerprint = next(filter(lambda line: line[:3] == "fpr", metadata.splitlines()))
|
||||||
return fingerprint.split(":")[-2]
|
return fingerprint.split(":")[-2]
|
||||||
|
except (IndexError, StopIteration):
|
||||||
|
raise GPGError(f"key {key} has invalid metadata") from None
|
||||||
|
|
||||||
def key_import(self, server: str, key: str) -> None:
|
def key_import(self, server: str, key: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -199,6 +199,19 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: not implemented method
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class LocalClient(Client):
|
|||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
repository_id(RepositoryId): repository unique identifier
|
repository_id(RepositoryId): repository unique identifier
|
||||||
database(SQLite): database instance:
|
database(SQLite): database instance
|
||||||
"""
|
"""
|
||||||
self.database = database
|
self.database = database
|
||||||
self.repository_id = repository_id
|
self.repository_id = repository_id
|
||||||
@@ -143,6 +143,16 @@ class LocalClient(Client):
|
|||||||
return packages
|
return packages
|
||||||
return [(package, status) for package, status in packages if package.base == package_base]
|
return [(package, status) for package, status in packages if package.base == package_base]
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
self.database.package_hold_update(package_base, self.repository_id, enabled=enabled)
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -18,11 +18,13 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import replace
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Self
|
from typing import Any, Self
|
||||||
|
|
||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.core.log import LazyLogging
|
from ahriman.core.log import LazyLogging
|
||||||
|
from ahriman.core.repository.package_info import PackageInfo
|
||||||
from ahriman.core.status import Client
|
from ahriman.core.status import Client
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
from ahriman.models.changes import Changes
|
from ahriman.models.changes import Changes
|
||||||
@@ -39,15 +41,18 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
client(Client): reporter instance
|
client(Client): reporter instance
|
||||||
|
package_info(PackageInfo): package info instance
|
||||||
status(BuildStatus): daemon status
|
status(BuildStatus): daemon status
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, client: Client) -> None:
|
def __init__(self, client: Client, package_info: PackageInfo) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
client(Client): reporter instance
|
client(Client): reporter instance
|
||||||
|
package_info(PackageInfo): package info instance
|
||||||
"""
|
"""
|
||||||
self.client = client
|
self.client = client
|
||||||
|
self.package_info = package_info
|
||||||
|
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||||
@@ -80,6 +85,18 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
logs_rotate: Callable[[int], None]
|
logs_rotate: Callable[[int], None]
|
||||||
|
|
||||||
|
def package_archives(self, package_base: str) -> list[Package]:
|
||||||
|
"""
|
||||||
|
get known package archives
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[Package]: list of built package for this package base
|
||||||
|
"""
|
||||||
|
return self.package_info.package_archives(package_base)
|
||||||
|
|
||||||
package_changes_get: Callable[[str], Changes]
|
package_changes_get: Callable[[str], Changes]
|
||||||
|
|
||||||
package_changes_update: Callable[[str, Changes], None]
|
package_changes_update: Callable[[str, Changes], None]
|
||||||
@@ -113,6 +130,19 @@ class Watcher(LazyLogging):
|
|||||||
|
|
||||||
package_logs_remove: Callable[[str, str | None], None]
|
package_logs_remove: Callable[[str, str | None], None]
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
package, status = self.package_get(package_base)
|
||||||
|
with self._lock:
|
||||||
|
self._known[package_base] = (package, replace(status, is_held=enabled))
|
||||||
|
self.client.package_hold_update(package_base, enabled=enabled)
|
||||||
|
|
||||||
package_patches_get: Callable[[str, str | None], list[PkgbuildPatch]]
|
package_patches_get: Callable[[str, str | None], list[PkgbuildPatch]]
|
||||||
|
|
||||||
package_patches_remove: Callable[[str, str], None]
|
package_patches_remove: Callable[[str, str], None]
|
||||||
@@ -138,9 +168,9 @@ class Watcher(LazyLogging):
|
|||||||
package_base(str): package base to update
|
package_base(str): package base to update
|
||||||
status(BuildStatusEnum): new build status
|
status(BuildStatusEnum): new build status
|
||||||
"""
|
"""
|
||||||
package, _ = self.package_get(package_base)
|
package, current_status = self.package_get(package_base)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._known[package_base] = (package, BuildStatus(status))
|
self._known[package_base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||||
self.client.package_status_update(package_base, status)
|
self.client.package_status_update(package_base, status)
|
||||||
|
|
||||||
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
|
def package_update(self, package: Package, status: BuildStatusEnum) -> None:
|
||||||
@@ -152,7 +182,8 @@ class Watcher(LazyLogging):
|
|||||||
status(BuildStatusEnum): new build status
|
status(BuildStatusEnum): new build status
|
||||||
"""
|
"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._known[package.base] = (package, BuildStatus(status))
|
_, current_status = self._known.get(package.base, (package, BuildStatus()))
|
||||||
|
self._known[package.base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||||
self.client.package_update(package, status)
|
self.client.package_update(package, status)
|
||||||
|
|
||||||
def status_update(self, status: BuildStatusEnum) -> None:
|
def status_update(self, status: BuildStatusEnum) -> None:
|
||||||
|
|||||||
@@ -314,6 +314,18 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||||
|
"""
|
||||||
|
update package hold status
|
||||||
|
|
||||||
|
Args:
|
||||||
|
package_base(str): package base name
|
||||||
|
enabled(bool): new hold status
|
||||||
|
"""
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.make_request("POST", f"{self.address}/api/v1/packages/{url_encode(package_base)}/hold",
|
||||||
|
params=self.repository_id.query(), json={"is_held": enabled})
|
||||||
|
|
||||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||||
"""
|
"""
|
||||||
post log record
|
post log record
|
||||||
|
|||||||
@@ -51,10 +51,12 @@ class BuildStatus:
|
|||||||
Attributes:
|
Attributes:
|
||||||
status(BuildStatusEnum): build status
|
status(BuildStatusEnum): build status
|
||||||
timestamp(int): build status update time
|
timestamp(int): build status update time
|
||||||
|
is_held(bool | None): whether package held or not
|
||||||
"""
|
"""
|
||||||
|
|
||||||
status: BuildStatusEnum = BuildStatusEnum.Unknown
|
status: BuildStatusEnum = BuildStatusEnum.Unknown
|
||||||
timestamp: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
timestamp: int = field(default_factory=lambda: int(utcnow().timestamp()))
|
||||||
|
is_held: bool | None = field(default=None, kw_only=True)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -83,7 +85,7 @@ class BuildStatus:
|
|||||||
Returns:
|
Returns:
|
||||||
str: print-friendly string
|
str: print-friendly string
|
||||||
"""
|
"""
|
||||||
return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
|
return f"{self.status.value} ({pretty_datetime(self.timestamp)}){" (held)" if self.is_held else ""}"
|
||||||
|
|
||||||
def view(self) -> dict[str, Any]:
|
def view(self) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -94,5 +96,6 @@ class BuildStatus:
|
|||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"status": self.status.value,
|
"status": self.status.value,
|
||||||
"timestamp": self.timestamp
|
"timestamp": self.timestamp,
|
||||||
|
"is_held": self.is_held,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,20 +31,12 @@ class Changes:
|
|||||||
Attributes:
|
Attributes:
|
||||||
last_commit_sha(str | None): last commit hash
|
last_commit_sha(str | None): last commit hash
|
||||||
changes(str | None): package change since the last commit if available
|
changes(str | None): package change since the last commit if available
|
||||||
|
pkgbuild(str | None): original PKGBUILD content if available
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_commit_sha: str | None = None
|
last_commit_sha: str | None = None
|
||||||
changes: str | None = None
|
changes: str | None = None
|
||||||
|
pkgbuild: str | None = None
|
||||||
@property
|
|
||||||
def is_empty(self) -> bool:
|
|
||||||
"""
|
|
||||||
validate that changes are not empty
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: ``True`` in case if changes are not set and ``False`` otherwise
|
|
||||||
"""
|
|
||||||
return self.changes is None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user