diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index 29153f57..6b509e43 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -11,9 +11,9 @@ assignees: ''
A clear and concise description of what the bug is.
-### Steps to Reproduce
+### Steps to reproduce
-Steps to reproduce the behavior (commands, environment etc)
+Steps to reproduce the behavior (commands, environment etc).
### Expected behavior
@@ -21,4 +21,8 @@ A clear and concise description of what you expected to happen.
### Logs
-Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option.
+Add logs to help explain your problem. By default, the application writes logs into `/dev/log` which is usually default systemd journal and can be accessed by `journalctl` command.
+
+You can also attach any additional information which can be helpful, e.g. configuration used by the application (be aware of passwords and other secrets if any); it can be generated by using `ahriman config` command.
+
+It is also sometimes useful to have information about installed packages which can be accessed by `ahriman version` command.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
index 63406d5b..b6daddd1 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -13,7 +13,7 @@ Brief description of the feature required
### Cause of the feature request
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+A clear and concise description of what the problem is. E.g. I'm always frustrated when [...]
### Proposed changes and/or features
diff --git a/docs/ahriman.1 b/docs/ahriman.1
index 8f690230..c17a063e 100644
--- a/docs/ahriman.1
+++ b/docs/ahriman.1
@@ -3,9 +3,9 @@
ahriman
.SH SYNOPSIS
.B ahriman
-[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,user-add,user-list,user-remove,web} ...
+[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ...
.SH DESCRIPTION
-ArcH Linux ReposItory MANager
+ArcH linux ReposItory MANager
.SH OPTIONS
.TP
@@ -37,7 +37,7 @@ force disable any logging
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
-\fB\-v\fR, \fB\-\-version\fR
+\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.SH
@@ -121,6 +121,9 @@ run triggers
\fBahriman\fR \fI\,repo-update\/\fR
update packages
.TP
+\fBahriman\fR \fI\,shell\/\fR
+envoke python shell
+.TP
\fBahriman\fR \fI\,user-add\/\fR
create or update user
.TP
@@ -130,6 +133,9 @@ user known users and their access
\fBahriman\fR \fI\,user-remove\/\fR
remove user
.TP
+\fBahriman\fR \fI\,version\/\fR
+application version
+.TP
\fBahriman\fR \fI\,web\/\fR
web server
.SH COMMAND \fI\,'ahriman aur-search'\/\fR
@@ -544,6 +550,11 @@ do not include manual updates
\fB\-\-no\-vcs\fR
do not check VCS packages
+.SH COMMAND \fI\,'ahriman shell'\/\fR
+usage: ahriman shell [-h]
+
+drop into python shell while having created application
+
.SH COMMAND \fI\,'ahriman user-add'\/\fR
usage: ahriman user-add [-h] [--as-service] [-p PASSWORD]
[-r {UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}] [-s]
@@ -606,6 +617,11 @@ username for web service
\fB\-s\fR, \fB\-\-secure\fR
set file permissions to user\-only
+.SH COMMAND \fI\,'ahriman version'\/\fR
+usage: ahriman version [-h]
+
+print application and its dependencies versions
+
.SH COMMAND \fI\,'ahriman web'\/\fR
usage: ahriman web [-h]
diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst
index 2a9976e2..889a85a1 100644
--- a/docs/ahriman.application.handlers.rst
+++ b/docs/ahriman.application.handlers.rst
@@ -116,6 +116,14 @@ ahriman.application.handlers.setup module
:no-undoc-members:
:show-inheritance:
+ahriman.application.handlers.shell module
+-----------------------------------------
+
+.. automodule:: ahriman.application.handlers.shell
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
ahriman.application.handlers.sign module
----------------------------------------
@@ -172,6 +180,14 @@ ahriman.application.handlers.users module
:no-undoc-members:
:show-inheritance:
+ahriman.application.handlers.versions module
+--------------------------------------------
+
+.. automodule:: ahriman.application.handlers.versions
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
ahriman.application.handlers.web module
---------------------------------------
diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst
index 528cb7a9..c3ecda6d 100644
--- a/docs/ahriman.core.formatters.rst
+++ b/docs/ahriman.core.formatters.rst
@@ -76,6 +76,14 @@ ahriman.core.formatters.user\_printer module
:no-undoc-members:
:show-inheritance:
+ahriman.core.formatters.version\_printer module
+-----------------------------------------------
+
+.. automodule:: ahriman.core.formatters.version_printer
+ :members:
+ :no-undoc-members:
+ :show-inheritance:
+
Module contents
---------------
diff --git a/docs/conf.py b/docs/conf.py
index 06882281..9df1ccff 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -104,5 +104,4 @@ autodoc_member_order = "groupwise"
autodoc_default_options = {
"no-undoc-members": True,
- "special-members": "__init__",
}
diff --git a/package/share/ahriman/templates/shell b/package/share/ahriman/templates/shell
new file mode 100644
index 00000000..0e851628
--- /dev/null
+++ b/package/share/ahriman/templates/shell
@@ -0,0 +1,18 @@
+ [00m
+ [38;5;60mâ–„[48;5;60;38;5;67mâ–„[49;38;5;60mâ–„[39m [38;5;60mâ–„â–„â–„â–„â–„â–„[48;5;60mâ–ˆ[49mâ–€[39m [00m
+ [38;5;60m▄[48;5;60;38;5;67m▄[48;5;67;38;5;60m▄[48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;60;38;5;103m▄[38;5;67m▄▄▄[48;5;67m██[38;5;60m▄▄▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [00m
+ [48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;110m██[48;5;103;38;5;103m█[48;5;60;38;5;67m▄▄▄▄[48;5;67;38;5;60m▄[38;5;67m██[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [00m
+ [48;5;60;38;5;60m█[48;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103;38;5;103m█[48;5;60;38;5;110m▄▄[38;5;103m▄[48;5;110;38;5;110m█[48;5;60;38;5;60m█[48;5;67;38;5;67m██[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m█[48;5;60m▄[49;38;5;60m▄[39m [38;5;60m▄[39m[00m
+ [48;5;60;38;5;60m█[38;5;67m▄[48;5;103;38;5;60m▄[48;5;110;38;5;103m▄[38;5;110m█████████[48;5;60m▄▄[48;5;67;38;5;60m▄▄[49m▀[48;5;67m▄[38;5;67m█[48;5;60m▄[38;5;60m█[49m▀[39m[00m
+ [48;5;60;38;5;60m█[49m▄▀[48;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m███[38;5;232m▄[38;5;110m█[38;5;232m▄▄▄▄[38;5;110m███[48;5;103m▄[49;38;5;103m▄[39m [38;5;60m▀▀[39m [00m
+ [48;5;60;38;5;60m█[48;5;67m▄[48;5;60;38;5;67m▄▄[48;5;103;38;5;103m█[48;5;110;38;5;110m███[38;5;232m▄[48;5;232m█[48;5;188;38;5;188m█[48;5;231;38;5;231m██[48;5;232;38;5;232m██[48;5;110;38;5;110m███[48;5;103;38;5;103m█[49;39m [00m
+ [38;5;60m▄▄[48;5;60;38;5;67m▄▄▄▄[49;38;5;60m▄▄▄[39m [38;5;60m▀[48;5;60m█[38;5;67m▄[48;5;103;38;5;103m█[48;5;110;38;5;110m████[48;5;232m▄[48;5;145;38;5;231m▄[48;5;232;38;5;145m▄[38;5;231m▄[38;5;232m█[38;5;231m▄[48;5;110;38;5;110m██[48;5;103;38;5;103m█[49m▄[39m [00m
+ [38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m███[38;5;60m▄▄▄▄▄▄[48;5;60;38;5;67m▄▄[49;38;5;60m▄[39m [38;5;60m▀[39m [38;5;103m▀[48;5;110m▄[38;5;110m█████[48;5;188m▄▄[48;5;110m█[38;5;103m▄[38;5;110m██[48;5;103;38;5;103m█[49;39m [00m
+ [48;5;60;38;5;60m█[48;5;67;38;5;67m██[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m████[38;5;60m▄▄[48;5;60m█[48;5;67m▄[38;5;67m█[48;5;60;38;5;60m█[49;38;5;103m▄▄▄[39m [48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103m▄[48;5;110;38;5;103m▄▄▄▄▄[48;5;103m█[49m▀▀[39m [00m
+ [48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60;38;5;60m█[48;5;67;38;5;67m████[38;5;60m▄[49m▀[39m [48;5;60;38;5;103m▄[48;5;103;38;5;110m▄[48;5;110;38;5;179m▄[38;5;110m██[48;5;103m▄▄▄[48;5;110m████[48;5;103;38;5;103m█[49;39m [00m
+ [48;5;60;38;5;60m█[48;5;67;38;5;67m█[48;5;60m▄[48;5;67;38;5;60m▄[38;5;67m███[48;5;60;38;5;60m█[49;39m [48;5;103;38;5;103m█[48;5;185;38;5;110m▄[48;5;110m█[38;5;179m▄[48;5;179;38;5;110m▄[48;5;110m████████[48;5;103;38;5;103m█[49;39m [00m
+ [38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m█[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m██[38;5;60m▄[49m▀[39m [38;5;103m▀[48;5;110m▄[38;5;110m████[38;5;103m▄[38;5;110m██[38;5;103m▄[38;5;110m██[38;5;103m▄[48;5;103;38;5;67m▄[49;39m [00m
+ [48;5;60;38;5;60m█[48;5;67;38;5;67m██[48;5;60;38;5;60m█[48;5;67;38;5;67m███[48;5;60;38;5;60m█[49;39m [38;5;103m▄[48;5;103m█[48;5;110;38;5;110m██[38;5;103m▄[48;5;103;38;5;110m▄[38;5;67m▄[38;5;103m█[38;5;110m▄[48;5;110m█[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;67;38;5;67m█[49;39m [00m
+ [38;5;60m▄[48;5;60;38;5;67m▄[48;5;67;38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m███[38;5;60m▄[49m▀[39m [38;5;103m▄[48;5;103;38;5;110m▄[48;5;110m██[48;5;103;38;5;103m█[48;5;110;38;5;110m█[48;5;67;38;5;67m█[49;39m [48;5;103;38;5;103m█[48;5;110;38;5;110m██[48;5;103m▄[48;5;110;38;5;103m▄[48;5;67;38;5;110m▄[49;38;5;67m▄[39m [00m
+ [38;5;60m▄▄▄[48;5;60m█[48;5;67;38;5;67m█[38;5;60m▄[48;5;60;38;5;67m▄[48;5;67m█[38;5;60m▄▄▄[49m▀[39m [48;5;103;38;5;103m█[48;5;110;38;5;110m███[48;5;103;38;5;103m█[48;5;110;38;5;67m▄[48;5;67m█[49;39m [48;5;103;38;5;103m█[48;5;110;38;5;110m███[48;5;103;38;5;103m█[48;5;110;38;5;67m▄[48;5;67m█[49;39m [00m
+ [38;5;60m▀▀▀▀▀▀▀[39m [38;5;103m▀▀▀▀[39m [38;5;103m▀▀▀▀[39m [00m
diff --git a/setup.py b/setup.py
index 772b9b6b..eaf3b083 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,7 @@ setup(
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/repo-index.jinja2",
+ "package/share/ahriman/templates/shell",
"package/share/ahriman/templates/telegram-index.jinja2",
]),
("share/ahriman/templates/build-status", [
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index f7c4de67..726223e5 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -26,6 +26,7 @@ from typing import List, TypeVar
from ahriman import version
from ahriman.application import handlers
+from ahriman.core.util import enum_values
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package_source import PackageSource
@@ -76,7 +77,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
- parser.add_argument("-v", "--version", action="version", version=version.__version__)
+ parser.add_argument("-V", "--version", action="version", version=version.__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
@@ -106,9 +107,11 @@ def _parser() -> argparse.ArgumentParser:
_set_repo_sync_parser(subparsers)
_set_repo_triggers_parser(subparsers)
_set_repo_update_parser(subparsers)
+ _set_shell_parser(subparsers)
_set_user_add_parser(subparsers)
_set_user_list_parser(subparsers)
_set_user_remove_parser(subparsers)
+ _set_version_parser(subparsers)
_set_web_parser(subparsers)
return parser
@@ -225,7 +228,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
- type=PackageSource, choices=PackageSource, default=PackageSource.Auto)
+ type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
parser.set_defaults(handler=handlers.Add)
return parser
@@ -267,7 +270,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
- type=BuildStatusEnum, choices=BuildStatusEnum)
+ type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@@ -309,7 +312,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
"If no packages supplied, service status will be updated",
nargs="*")
parser.add_argument("-s", "--status", help="new status",
- type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
+ type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True,
unsafe=True)
return parser
@@ -556,7 +559,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
- type=SignSettings.from_option, choices=SignSettings)
+ type=SignSettings.from_option, choices=enum_values(SignSettings))
parser.add_argument("--web-port", help="port of the web service", type=int)
parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@@ -594,7 +597,7 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter)
parser.add_argument("-s", "--status", help="new status",
- type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success)
+ type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success)
parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[],
quiet=True, unsafe=True)
return parser
@@ -661,6 +664,24 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
+def _set_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for shell subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("shell", help="envoke python shell",
+ description="drop into python shell while having created application",
+ formatter_class=_formatter)
+ parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
+ parser.set_defaults(handler=handlers.Shell, lock=None, no_report=True)
+ return parser
+
+
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
@@ -680,7 +701,7 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, "
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-r", "--role", help="user access level",
- type=UserAccess, choices=UserAccess, default=UserAccess.Read)
+ type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true")
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, no_report=True,
quiet=True, unsafe=True)
@@ -702,7 +723,7 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
formatter_class=_formatter)
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
- parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess)
+ parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec
password="", quiet=True, unsafe=True)
return parser
@@ -728,6 +749,23 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
+def _set_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
+ """
+ add parser for version subcommand
+
+ Args:
+ root(SubParserAction): subparsers for the commands
+
+ Returns:
+ argparse.ArgumentParser: created argument parser
+ """
+ parser = root.add_parser("version", help="application version",
+ description="print application and its dependencies versions", formatter_class=_formatter)
+ parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, no_report=True, quiet=True,
+ unsafe=True)
+ return parser
+
+
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand
diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py
index 4d78ed17..6c72ad46 100644
--- a/src/ahriman/application/handlers/__init__.py
+++ b/src/ahriman/application/handlers/__init__.py
@@ -32,6 +32,7 @@ from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.setup import Setup
+from ahriman.application.handlers.shell import Shell
from ahriman.application.handlers.sign import Sign
from ahriman.application.handlers.status import Status
from ahriman.application.handlers.status_update import StatusUpdate
@@ -39,4 +40,5 @@ from ahriman.application.handlers.triggers import Triggers
from ahriman.application.handlers.unsafe_commands import UnsafeCommands
from ahriman.application.handlers.update import Update
from ahriman.application.handlers.users import Users
+from ahriman.application.handlers.versions import Versions
from ahriman.application.handlers.web import Web
diff --git a/src/ahriman/application/handlers/shell.py b/src/ahriman/application/handlers/shell.py
new file mode 100644
index 00000000..80149032
--- /dev/null
+++ b/src/ahriman/application/handlers/shell.py
@@ -0,0 +1,59 @@
+#
+# Copyright (c) 2021-2022 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 .
+#
+import argparse
+import code
+import sys
+
+from pathlib import Path
+from typing import Type
+
+from ahriman.application.application import Application
+from ahriman.application.handlers import Handler
+from ahriman.core.configuration import Configuration
+from ahriman.core.formatters import StringPrinter
+
+
+class Shell(Handler):
+ """
+ python shell handler
+ """
+
+ ALLOW_MULTI_ARCHITECTURE_RUN = False
+
+ @classmethod
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool, unsafe: bool) -> None:
+ """
+ callback for command line
+
+ Args:
+ args(argparse.Namespace): command line args
+ architecture(str): repository architecture
+ configuration(Configuration): configuration instance
+ no_report(bool): force disable reporting
+ unsafe(bool): if set no user check will be performed before path creation
+ """
+ # pylint: disable=possibly-unused-variable
+ application = Application(architecture, configuration, no_report, unsafe)
+ if args.verbose:
+ # licensed by https://creativecommons.org/licenses/by-sa/3.0
+ path = Path(sys.prefix) / "share" / "ahriman" / "templates" / "shell"
+ StringPrinter(path.read_text(encoding="utf8")).print(verbose=False)
+ code.interact(local=locals())
diff --git a/src/ahriman/application/handlers/versions.py b/src/ahriman/application/handlers/versions.py
new file mode 100644
index 00000000..e849198e
--- /dev/null
+++ b/src/ahriman/application/handlers/versions.py
@@ -0,0 +1,87 @@
+#
+# Copyright (c) 2021-2022 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 .
+#
+import argparse
+import pkg_resources
+import sys
+
+from typing import Dict, List, Tuple, Type
+
+from ahriman import version
+from ahriman.application.handlers import Handler
+from ahriman.core.configuration import Configuration
+from ahriman.core.formatters import VersionPrinter
+
+
+class Versions(Handler):
+ """
+ version handler
+ """
+
+ ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
+
+ @classmethod
+ def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
+ configuration: Configuration, no_report: bool, unsafe: bool) -> None:
+ """
+ callback for command line
+
+ Args:
+ args(argparse.Namespace): command line args
+ architecture(str): repository architecture
+ configuration(Configuration): configuration instance
+ no_report(bool): force disable reporting
+ unsafe(bool): if set no user check will be performed before path creation
+ """
+ VersionPrinter(f"Module version {version.__version__}",
+ {"Python": sys.version}).print(verbose=False, separator=" ")
+ packages = Versions.package_dependencies("ahriman", ("pacman", "s3", "web"))
+ VersionPrinter("Installed packages", packages).print(verbose=False, separator=" ")
+
+ @staticmethod
+ def package_dependencies(root: str, root_extras: Tuple[str, ...] = ()) -> Dict[str, str]:
+ """
+ extract list of ahriman package dependencies installed into system with their versions
+
+ Args:
+ root(str): root package name
+ root_extras(Tuple[str, ...]): extras for the root package (Default value = ())
+
+ Returns:
+ Dict[str, str]: map of installed dependency to its version
+ """
+ resources: Dict[str, pkg_resources.Distribution] = pkg_resources.working_set.by_key # type: ignore
+
+ def dependencies_by_key(key: str, extras: Tuple[str, ...] = ()) -> List[str]:
+ return [entry.key for entry in resources[key].requires(extras)]
+
+ keys: List[str] = []
+ portion = {key for key in dependencies_by_key(root, root_extras) if key in resources}
+ while portion:
+ keys.extend(portion)
+ portion = {
+ key
+ for key in sum([dependencies_by_key(key) for key in portion], start=[])
+ if key not in keys and key in resources
+ }
+
+ return {
+ resource.project_name: resource.version
+ for resource in map(lambda key: resources[key], keys)
+ }
diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py
index fea8b9d5..931e99d5 100644
--- a/src/ahriman/core/formatters/__init__.py
+++ b/src/ahriman/core/formatters/__init__.py
@@ -27,3 +27,4 @@ from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.update_printer import UpdatePrinter
from ahriman.core.formatters.user_printer import UserPrinter
+from ahriman.core.formatters.version_printer import VersionPrinter
diff --git a/src/ahriman/core/formatters/string_printer.py b/src/ahriman/core/formatters/string_printer.py
index d9763f7b..70466bfe 100644
--- a/src/ahriman/core/formatters/string_printer.py
+++ b/src/ahriman/core/formatters/string_printer.py
@@ -25,6 +25,9 @@ from ahriman.core.formatters import Printer
class StringPrinter(Printer):
"""
print content of the random string
+
+ Attributes:
+ content(str): any content string
"""
def __init__(self, content: str) -> None:
diff --git a/src/ahriman/core/formatters/version_printer.py b/src/ahriman/core/formatters/version_printer.py
new file mode 100644
index 00000000..d2b0829a
--- /dev/null
+++ b/src/ahriman/core/formatters/version_printer.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2021-2022 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 .
+#
+from typing import Dict, List
+
+from ahriman.core.formatters import StringPrinter
+from ahriman.models.property import Property
+
+
+class VersionPrinter(StringPrinter):
+ """
+ print content of the python package versions
+
+ Attributes:
+ packages(Dict[str, str]): map of package name to its version
+ """
+
+ def __init__(self, title: str, packages: Dict[str, str]) -> None:
+ """
+ default constructor
+
+ Args:
+ title(str): title of the message
+ packages(Dict[str, str]): map of package name to its version
+ """
+ StringPrinter.__init__(self, title)
+ self.packages = packages
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+
+ Returns:
+ List[Property]: list of content properties
+ """
+ return [
+ Property(package, version, is_required=True)
+ for package, version in sorted(self.packages.items())
+ ]
diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py
index df9e6485..40bef688 100644
--- a/src/ahriman/core/util.py
+++ b/src/ahriman/core/util.py
@@ -18,7 +18,10 @@
# along with this program. If not, see .
#
import datetime
+import io
import os
+from enum import Enum
+
import requests
import shutil
import subprocess
@@ -27,14 +30,14 @@ import tempfile
from contextlib import contextmanager
from logging import Logger
from pathlib import Path
-from typing import Any, Dict, Generator, Iterable, List, Optional, Union
+from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union
from ahriman.core.exceptions import InvalidOption, UnsafeRun
from ahriman.models.repository_paths import RepositoryPaths
-__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "package_like",
- "pretty_datetime", "pretty_size", "tmpdir", "walk"]
+__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "enum_values",
+ "package_like", "pretty_datetime", "pretty_size", "tmpdir", "walk"]
def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None,
@@ -73,6 +76,11 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
>>> check_output("false", exception=RuntimeError("An exception occurred"))
"""
+ # hack for Optional[IO[str]] handle
+ def get_io(proc: subprocess.Popen[str], channel_name: str) -> IO[str]:
+ channel: Optional[IO[str]] = getattr(proc, channel_name, None)
+ return channel if channel is not None else io.StringIO()
+
def log(single: str) -> None:
if logger is not None:
logger.debug(single)
@@ -80,14 +88,15 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
# FIXME additional workaround for linter and type check which do not know that user arg is supported
# pylint: disable=unexpected-keyword-arg
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- user=user, text=True, encoding="utf8", bufsize=1) as process: # type: ignore
+ user=user, text=True, encoding="utf8", bufsize=1) as process:
if input_data is not None:
- process.stdin.write(input_data)
- process.stdin.close()
+ input_channel = get_io(process, "stdin")
+ input_channel.write(input_data)
+ input_channel.close()
# read stdout and append to output result
result: List[str] = []
- for line in iter(process.stdout.readline, ""):
+ for line in iter(get_io(process, "stdout").readline, ""):
line = line.strip()
if not line: # skip empty lines
continue
@@ -95,7 +104,7 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
log(line)
# read stderr and write info to logs
- for line in iter(process.stderr.readline, ""):
+ for line in iter(get_io(process, "stderr").readline, ""):
log(line.strip())
process.terminate() # make sure that process is terminated
@@ -134,6 +143,19 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
raise UnsafeRun(current_uid, root_uid)
+def enum_values(enum: Type[Enum]) -> List[str]:
+ """
+ generate list of enumeration values from the source
+
+ Args:
+ enum(Type[Enum]): source enumeration class
+
+ Returns:
+ List[str]: available enumeration values as string
+ """
+ return [key.value for key in enum]
+
+
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
"""
safe response exception text generation
diff --git a/tests/ahriman/application/handlers/test_handler_shell.py b/tests/ahriman/application/handlers/test_handler_shell.py
new file mode 100644
index 00000000..03d2b82c
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_shell.py
@@ -0,0 +1,48 @@
+import argparse
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Shell
+from ahriman.core.configuration import Configuration
+
+
+def _default_args(args: argparse.Namespace) -> argparse.Namespace:
+ """
+ default arguments for these test cases
+
+ Args:
+ args(argparse.Namespace): command line arguments fixture
+
+ Returns:
+ argparse.Namespace: generated arguments for these test cases
+ """
+ args.verbose = False
+ return args
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args = _default_args(args)
+ mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ application_mock = mocker.patch("code.interact")
+
+ Shell.run(args, "x86_64", configuration, True, False)
+ application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
+
+
+def test_run_verbose(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command with verbose option
+ """
+ args = _default_args(args)
+ args.verbose = True
+ mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
+ application_mock = mocker.patch("code.interact")
+
+ Shell.run(args, "x86_64", configuration, True, False)
+ application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
+ print_mock.assert_called_once_with(verbose=False)
diff --git a/tests/ahriman/application/handlers/test_handler_versions.py b/tests/ahriman/application/handlers/test_handler_versions.py
new file mode 100644
index 00000000..d1140a94
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_versions.py
@@ -0,0 +1,38 @@
+import argparse
+
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.application.handlers import Versions
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ application_mock = mocker.patch("ahriman.application.handlers.Versions.package_dependencies")
+ print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
+
+ Versions.run(args, "x86_64", configuration, True, False)
+ application_mock.assert_called_once_with("ahriman", ("pacman", "s3", "web"))
+ print_mock.assert_has_calls([mock.call(verbose=False, separator=" "), mock.call(verbose=False, separator=" ")])
+
+
+def test_package_dependencies() -> None:
+ """
+ must extract package dependencies
+ """
+ packages = Versions.package_dependencies("srcinfo")
+ assert packages
+ assert packages.get("parse") is not None
+
+
+def test_package_dependencies_missing() -> None:
+ """
+ must extract package dependencies even if some of them are missing
+ """
+ packages = Versions.package_dependencies("ahriman", ("docs", "pacman", "s3", "web"))
+ assert packages
+ assert packages.get("pyalpm") is not None
+ assert packages.get("Sphinx") is None
diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py
index 3d4a9826..8d04c14e 100644
--- a/tests/ahriman/application/test_ahriman.py
+++ b/tests/ahriman/application/test_ahriman.py
@@ -492,6 +492,15 @@ def test_subparsers_repo_update_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == ["x86_64"]
+def test_subparsers_shell(parser: argparse.ArgumentParser) -> None:
+ """
+ shell command must imply lock and no-report
+ """
+ args = parser.parse_args(["shell"])
+ assert args.lock is None
+ assert args.no_report
+
+
def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None:
"""
user-add command must imply action, architecture, lock, no-report, quiet and unsafe
@@ -575,6 +584,26 @@ def test_subparsers_user_remove_architecture(parser: argparse.ArgumentParser) ->
assert args.architecture == [""]
+def test_subparsers_version(parser: argparse.ArgumentParser) -> None:
+ """
+ version command must imply architecture, lock, no-report, quiet and unsafe
+ """
+ args = parser.parse_args(["version"])
+ assert args.architecture == [""]
+ assert args.lock is None
+ assert args.no_report
+ assert args.quiet
+ assert args.unsafe
+
+
+def test_subparsers_version_architecture(parser: argparse.ArgumentParser) -> None:
+ """
+ version command must correctly parse architecture list
+ """
+ args = parser.parse_args(["-a", "x86_64", "version"])
+ assert args.architecture == [""]
+
+
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
"""
web command must imply lock, no_report and parser
diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py
index d362e40d..7246d195 100644
--- a/tests/ahriman/core/formatters/conftest.py
+++ b/tests/ahriman/core/formatters/conftest.py
@@ -1,6 +1,7 @@
import pytest
-from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, UpdatePrinter, UserPrinter
+from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \
+ UpdatePrinter, UserPrinter, VersionPrinter
from ahriman.models.aur_package import AURPackage
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@@ -94,3 +95,17 @@ def user_printer(user: User) -> UserPrinter:
UserPrinter: user printer test instance
"""
return UserPrinter(user)
+
+
+@pytest.fixture
+def version_printer(package_ahriman: Package) -> VersionPrinter:
+ """
+ fixture for version printer
+
+ Args:
+ package_ahriman(Package): package fixture
+
+ Returns:
+ VersionPrinter: version printer test instance
+ """
+ return VersionPrinter("package", {package_ahriman.base: package_ahriman.version})
diff --git a/tests/ahriman/core/formatters/test_version_printer.py b/tests/ahriman/core/formatters/test_version_printer.py
new file mode 100644
index 00000000..4483eb5e
--- /dev/null
+++ b/tests/ahriman/core/formatters/test_version_printer.py
@@ -0,0 +1,15 @@
+from ahriman.core.formatters import VersionPrinter
+
+
+def test_properties(version_printer: VersionPrinter) -> None:
+ """
+ must return empty properties list
+ """
+ assert version_printer.properties()
+
+
+def test_title(version_printer: VersionPrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert version_printer.title() is not None
diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py
index bd125228..1a9c4ddf 100644
--- a/tests/ahriman/core/test_util.py
+++ b/tests/ahriman/core/test_util.py
@@ -10,8 +10,9 @@ from unittest.mock import MagicMock
from ahriman.core.exceptions import BuildFailed, InvalidOption, UnsafeRun
from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, full_version, \
- package_like, pretty_datetime, pretty_size, tmpdir, walk
+ enum_values, package_like, pretty_datetime, pretty_size, tmpdir, walk
from ahriman.models.package import Package
+from ahriman.models.package_source import PackageSource
from ahriman.models.repository_paths import RepositoryPaths
@@ -177,6 +178,15 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None:
assert "base" not in filter_json(probe, probe.keys())
+def test_enum_values() -> None:
+ """
+ must correctly generate choices from enumeration classes
+ """
+ values = enum_values(PackageSource)
+ for value in values:
+ assert PackageSource(value).value == value
+
+
def test_full_version() -> None:
"""
must construct full version
@@ -331,6 +341,7 @@ def test_walk(resource_path_root: Path) -> None:
resource_path_root / "web" / "templates" / "build-status.jinja2",
resource_path_root / "web" / "templates" / "email-index.jinja2",
resource_path_root / "web" / "templates" / "repo-index.jinja2",
+ resource_path_root / "web" / "templates" / "shell",
resource_path_root / "web" / "templates" / "telegram-index.jinja2",
])
local_files = list(sorted(walk(resource_path_root)))