mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-07-24 03:11:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3d1471e0 | ||
|
|
f37c5bd2a2 | ||
|
|
bd26e8da68 | ||
|
|
99365ea78f | ||
|
|
2eb51ba85a | ||
|
|
36e28aba99 |
@@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2026\-07\-21" "ahriman 2.21.0" "ArcH linux ReposItory MANager"
|
.TH AHRIMAN "1" "2026\-07\-21" "ahriman 2.21.1" "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.21.0"
|
__version__ = "2.21.1"
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
from logging import NullHandler
|
from logging import NullHandler
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ahriman.core.module_loader import optional_module
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["JournalHandler"]
|
__all__ = ["JournalHandler"]
|
||||||
|
|
||||||
@@ -40,7 +42,5 @@ class _JournalHandler(NullHandler):
|
|||||||
del args, kwargs
|
del args, kwargs
|
||||||
|
|
||||||
|
|
||||||
try:
|
systemd_journal = optional_module("systemd.journal")
|
||||||
from systemd.journal import JournalHandler # type: ignore[import-untyped]
|
JournalHandler = systemd_journal.JournalHandler if systemd_journal else _JournalHandler
|
||||||
except ImportError:
|
|
||||||
JournalHandler = _JournalHandler
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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.core.log.log_context import LogContext
|
||||||
|
from ahriman.core.module_loader import optional_module
|
||||||
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
|
||||||
|
|
||||||
@@ -65,11 +66,8 @@ class LogLoader:
|
|||||||
if selected is not None:
|
if selected is not None:
|
||||||
return selected
|
return selected
|
||||||
|
|
||||||
try:
|
if optional_module("systemd.journal"):
|
||||||
from systemd.journal import JournalHandler # type: ignore[import-untyped]
|
|
||||||
del JournalHandler
|
|
||||||
return LogHandler.Journald # journald import was found
|
return LogHandler.Journald # journald import was found
|
||||||
except ImportError:
|
|
||||||
if LogLoader.DEFAULT_SYSLOG_DEVICE.exists():
|
if LogLoader.DEFAULT_SYSLOG_DEVICE.exists():
|
||||||
return LogHandler.Syslog
|
return LogHandler.Syslog
|
||||||
return LogHandler.Console
|
return LogHandler.Console
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ from pkgutil import ModuleInfo, walk_packages
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, TypeGuard, TypeVar
|
from typing import Any, TypeGuard, TypeVar
|
||||||
|
|
||||||
|
__all__ = ["implementations", "optional_module"]
|
||||||
__all__ = ["implementations"]
|
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -74,3 +73,19 @@ def implementations(root_module: ModuleType, base_class: type[T]) -> Iterator[ty
|
|||||||
|
|
||||||
for _, attribute in inspect.getmembers(module, is_base_class):
|
for _, attribute in inspect.getmembers(module, is_base_class):
|
||||||
yield attribute
|
yield attribute
|
||||||
|
|
||||||
|
|
||||||
|
def optional_module(module_name: str) -> ModuleType | None:
|
||||||
|
"""
|
||||||
|
import an optional module
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module_name(str): fully qualified module name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ModuleType | None: imported module or ``None`` when it cannot be imported
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return import_module(module_name)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ahriman.web.views
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ahriman.core.module_loader import _modules, implementations
|
from ahriman.core.module_loader import _modules, implementations, optional_module
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
@@ -23,3 +23,17 @@ def test_implementations() -> None:
|
|||||||
assert routes
|
assert routes
|
||||||
assert all(isinstance(view, type) for view in routes)
|
assert all(isinstance(view, type) for view in routes)
|
||||||
assert all(issubclass(view, BaseView) for view in routes)
|
assert all(issubclass(view, BaseView) for view in routes)
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_module() -> None:
|
||||||
|
"""
|
||||||
|
must import an available module
|
||||||
|
"""
|
||||||
|
assert optional_module("ahriman.web.views") is ahriman.web.views
|
||||||
|
|
||||||
|
|
||||||
|
def test_optional_module_fallback() -> None:
|
||||||
|
"""
|
||||||
|
must return none when the module cannot be imported
|
||||||
|
"""
|
||||||
|
assert optional_module("missing_ahriman_module") is None
|
||||||
|
|||||||
@@ -17,18 +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/>.
|
||||||
#
|
#
|
||||||
try:
|
|
||||||
import aiohttp_security
|
|
||||||
except ImportError:
|
|
||||||
aiohttp_security = None # type: ignore[assignment]
|
|
||||||
|
|
||||||
try:
|
|
||||||
import aiohttp_session
|
|
||||||
except ImportError:
|
|
||||||
aiohttp_session = None # type: ignore[assignment]
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ahriman.core.module_loader import optional_module
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"authorized_userid",
|
"authorized_userid",
|
||||||
@@ -39,6 +31,10 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
aiohttp_security = optional_module("aiohttp_security")
|
||||||
|
aiohttp_session = optional_module("aiohttp_session")
|
||||||
|
|
||||||
|
|
||||||
async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
handle aiohttp security methods
|
handle aiohttp security methods
|
||||||
@@ -50,7 +46,7 @@ async def authorized_userid(*args: Any, **kwargs: Any) -> Any:
|
|||||||
Returns:
|
Returns:
|
||||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_security is not None:
|
if aiohttp_security:
|
||||||
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.authorized_userid(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -66,7 +62,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
|
|||||||
Returns:
|
Returns:
|
||||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_security is not None:
|
if aiohttp_security:
|
||||||
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.check_authorized(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -82,7 +78,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
|
|||||||
Returns:
|
Returns:
|
||||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_security is not None:
|
if aiohttp_security:
|
||||||
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.forget(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -98,7 +94,7 @@ async def get_session(*args: Any, **kwargs: Any) -> Any:
|
|||||||
Returns:
|
Returns:
|
||||||
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
|
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_session is not None:
|
if aiohttp_session:
|
||||||
return await aiohttp_session.get_session(*args, **kwargs)
|
return await aiohttp_session.get_session(*args, **kwargs)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -114,6 +110,6 @@ async def remember(*args: Any, **kwargs: Any) -> Any:
|
|||||||
Returns:
|
Returns:
|
||||||
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_security is not None:
|
if aiohttp_security:
|
||||||
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
return await aiohttp_security.remember(*args, **kwargs) # pylint: disable=no-value-for-parameter
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -17,15 +17,20 @@
|
|||||||
# 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/>.
|
||||||
#
|
#
|
||||||
try:
|
from ahriman.core.module_loader import optional_module
|
||||||
import aiohttp_apispec # type: ignore[import-untyped]
|
|
||||||
|
|
||||||
from marshmallow import Schema, fields
|
|
||||||
except ImportError:
|
aiohttp_apispec = optional_module("aiohttp_apispec")
|
||||||
|
marshmallow = optional_module("marshmallow")
|
||||||
|
|
||||||
|
|
||||||
|
if aiohttp_apispec and marshmallow:
|
||||||
|
Schema = marshmallow.Schema
|
||||||
|
fields = marshmallow.fields
|
||||||
|
else:
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
Schema = Mock # type: ignore[misc]
|
Schema = Mock # type: ignore[misc]
|
||||||
aiohttp_apispec = None
|
|
||||||
fields = Mock()
|
fields = Mock()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ def apidocs(*,
|
|||||||
authorization_required = permission != UserAccess.Unauthorized
|
authorization_required = permission != UserAccess.Unauthorized
|
||||||
|
|
||||||
def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]:
|
def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
if aiohttp_apispec is None:
|
if not aiohttp_apispec:
|
||||||
return handler # apispec is disabled
|
return handler # apispec is disabled
|
||||||
|
|
||||||
responses = _response_schema(
|
responses = _response_schema(
|
||||||
|
|||||||
@@ -100,20 +100,17 @@ def _servers(application: Application) -> list[dict[str, Any]]:
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
|
|
||||||
def setup_apispec(application: Application) -> Any:
|
def setup_apispec(application: Application) -> None:
|
||||||
"""
|
"""
|
||||||
setup swagger api specification
|
setup swagger api specification
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
application(Application): web application instance
|
application(Application): web application instance
|
||||||
|
|
||||||
Returns:
|
|
||||||
Any: created specification instance if module is available
|
|
||||||
"""
|
"""
|
||||||
if aiohttp_apispec is None:
|
if aiohttp_apispec is None:
|
||||||
return None
|
return
|
||||||
|
|
||||||
return aiohttp_apispec.setup_aiohttp_apispec(
|
aiohttp_apispec.setup_aiohttp_apispec(
|
||||||
application,
|
application,
|
||||||
url="/api-docs/swagger.json",
|
url="/api-docs/swagger.json",
|
||||||
openapi_version="3.0.2",
|
openapi_version="3.0.2",
|
||||||
|
|||||||
@@ -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/>.
|
||||||
#
|
#
|
||||||
try:
|
|
||||||
import aiohttp_openmetrics
|
|
||||||
except ImportError:
|
|
||||||
aiohttp_openmetrics = None # type: ignore[assignment]
|
|
||||||
|
|
||||||
from aiohttp.typedefs import Middleware
|
from aiohttp.typedefs import Middleware
|
||||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware
|
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware
|
||||||
|
|
||||||
|
from ahriman.core.module_loader import optional_module
|
||||||
from ahriman.web.middlewares import HandlerType
|
from ahriman.web.middlewares import HandlerType
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +30,9 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
aiohttp_openmetrics = optional_module("aiohttp_openmetrics")
|
||||||
|
|
||||||
|
|
||||||
async def metrics(request: Request) -> Response:
|
async def metrics(request: Request) -> Response:
|
||||||
"""
|
"""
|
||||||
handler for returning metrics
|
handler for returning metrics
|
||||||
@@ -47,7 +46,7 @@ async def metrics(request: Request) -> Response:
|
|||||||
Raises:
|
Raises:
|
||||||
HTTPNotFound: endpoint is disabled
|
HTTPNotFound: endpoint is disabled
|
||||||
"""
|
"""
|
||||||
if aiohttp_openmetrics is None:
|
if not aiohttp_openmetrics:
|
||||||
raise HTTPNotFound
|
raise HTTPNotFound
|
||||||
return await aiohttp_openmetrics.metrics(request)
|
return await aiohttp_openmetrics.metrics(request)
|
||||||
|
|
||||||
@@ -59,7 +58,7 @@ def metrics_handler() -> Middleware:
|
|||||||
Returns:
|
Returns:
|
||||||
Middleware: middleware function to handle server metrics
|
Middleware: middleware function to handle server metrics
|
||||||
"""
|
"""
|
||||||
if aiohttp_openmetrics is not None:
|
if aiohttp_openmetrics:
|
||||||
return aiohttp_openmetrics.metrics_middleware
|
return aiohttp_openmetrics.metrics_middleware
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ async def server_info(view: BaseView) -> dict[str, Any]:
|
|||||||
"username": await authorized_userid(view.request),
|
"username": await authorized_userid(view.request),
|
||||||
},
|
},
|
||||||
"autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
|
"autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
|
||||||
"docs_enabled": aiohttp_apispec is not None,
|
"docs_enabled": bool(aiohttp_apispec),
|
||||||
"index_url": view.configuration.get("web", "index_url", fallback=None),
|
"index_url": view.configuration.get("web", "index_url", fallback=None),
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class DocsView(BaseView):
|
|||||||
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
||||||
and returns empty list otherwise
|
and returns empty list otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_apispec is None:
|
if not aiohttp_apispec:
|
||||||
return []
|
return []
|
||||||
return cls.ROUTES
|
return cls.ROUTES
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class SwaggerView(BaseView):
|
|||||||
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set
|
||||||
and returns empty list otherwise
|
and returns empty list otherwise
|
||||||
"""
|
"""
|
||||||
if aiohttp_apispec is None:
|
if not aiohttp_apispec:
|
||||||
return []
|
return []
|
||||||
return cls.ROUTES
|
return cls.ROUTES
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from secrets import token_urlsafe
|
|||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from ahriman.core.auth.helpers import get_session, remember
|
from ahriman.core.auth.helpers import get_session, remember
|
||||||
|
from ahriman.core.module_loader import optional_module
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
from ahriman.web.schemas import LoginSchema, OAuth2Schema
|
from ahriman.web.schemas import LoginSchema, OAuth2Schema
|
||||||
@@ -62,14 +63,12 @@ class LoginView(BaseView):
|
|||||||
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
|
HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled
|
||||||
HTTPUnauthorized: if case of authorization error
|
HTTPUnauthorized: if case of authorization error
|
||||||
"""
|
"""
|
||||||
try:
|
oauth = optional_module("ahriman.core.auth.oauth")
|
||||||
from ahriman.core.auth.oauth import OAuth
|
if not oauth:
|
||||||
except ImportError:
|
|
||||||
# no aioauth library found
|
|
||||||
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||||
|
|
||||||
oauth_provider = self.validator
|
oauth_provider = self.validator
|
||||||
if not isinstance(oauth_provider, OAuth):
|
if not isinstance(oauth_provider, oauth.OAuth):
|
||||||
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
|
||||||
|
|
||||||
session = await get_session(self.request)
|
session = await get_session(self.request)
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None:
|
|||||||
must set api specification
|
must set api specification
|
||||||
"""
|
"""
|
||||||
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec")
|
||||||
assert setup_apispec(application)
|
|
||||||
|
setup_apispec(application)
|
||||||
apispec_mock.assert_called_once_with(
|
apispec_mock.assert_called_once_with(
|
||||||
application,
|
application,
|
||||||
url="/api-docs/swagger.json",
|
url="/api-docs/swagger.json",
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async def test_get_import_error(client_with_auth: TestClient, mocker: MockerFixt
|
|||||||
"""
|
"""
|
||||||
must return 405 on import error
|
must return 405 on import error
|
||||||
"""
|
"""
|
||||||
pytest.helpers.import_error("ahriman.core.auth.oauth", ["OAuth"], mocker)
|
mocker.patch("ahriman.web.views.v1.user.login.optional_module", return_value=None)
|
||||||
response = await client_with_auth.get("/api/v1/login")
|
response = await client_with_auth.get("/api/v1/login")
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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.21.0
|
pkgver=2.21.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -39,5 +39,5 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.21.0"
|
"version": "2.21.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,11 +58,7 @@ export default function PackageInfoDialog({
|
|||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [localPackageBase, setLocalPackageBase] = useState(packageBase);
|
const localPackageBase = packageBase;
|
||||||
if (packageBase !== null && packageBase !== localPackageBase) {
|
|
||||||
setLocalPackageBase(packageBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
const [activeTab, setActiveTab] = useState<TabKey>("logs");
|
||||||
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
const [refreshDatabase, setRefreshDatabase] = useState(true);
|
||||||
|
|
||||||
@@ -157,38 +153,42 @@ export default function PackageInfoDialog({
|
|||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
{pkg &&
|
{pkg &&
|
||||||
<>
|
|
||||||
<PackageDetailsGrid dependencies={dependencies} pkg={pkg} />
|
<PackageDetailsGrid dependencies={dependencies} pkg={pkg} />
|
||||||
|
}
|
||||||
|
{localPackageBase &&
|
||||||
<PackagePatchesList
|
<PackagePatchesList
|
||||||
editable={isAuthorized}
|
editable={isAuthorized}
|
||||||
onDelete={key => void handleDeletePatch(key)}
|
onDelete={key => void handleDeletePatch(key)}
|
||||||
patches={patches}
|
patches={patches}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{localPackageBase && currentRepository &&
|
||||||
|
<>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: "divider", mt: 2 }}>
|
||||||
<Tabs onChange={(_, tab: TabKey) => setActiveTab(tab)} value={activeTab}>
|
<Tabs onChange={(_, tab: TabKey) => setActiveTab(tab)} value={activeTab}>
|
||||||
{tabs.map(({ key, label }) => <Tab key={key} label={label} value={key} />)}
|
{tabs.map(({ key, label }) => <Tab key={key} label={label} value={key} />)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{activeTab === "logs" && localPackageBase && currentRepository &&
|
{activeTab === "logs" &&
|
||||||
<BuildLogsTab
|
<BuildLogsTab
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={currentRepository}
|
repository={currentRepository}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{activeTab === "changes" && localPackageBase && currentRepository &&
|
{activeTab === "changes" &&
|
||||||
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
<ChangesTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{activeTab === "pkgbuild" && localPackageBase && currentRepository &&
|
{activeTab === "pkgbuild" &&
|
||||||
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
<PkgbuildTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{activeTab === "events" && localPackageBase && currentRepository &&
|
{activeTab === "events" &&
|
||||||
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
<EventsTab packageBase={localPackageBase} repository={currentRepository} />
|
||||||
}
|
}
|
||||||
{activeTab === "artifacts" && localPackageBase && currentRepository &&
|
{activeTab === "artifacts" &&
|
||||||
<ArtifactsTab
|
<ArtifactsTab
|
||||||
currentVersion={pkg.version}
|
currentVersion={pkg?.version}
|
||||||
packageBase={localPackageBase}
|
packageBase={localPackageBase}
|
||||||
repository={currentRepository}
|
repository={currentRepository}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { useCallback, useMemo } from "react";
|
|||||||
import { DETAIL_TABLE_PROPS } from "utils";
|
import { DETAIL_TABLE_PROPS } from "utils";
|
||||||
|
|
||||||
interface ArtifactsTabProps {
|
interface ArtifactsTabProps {
|
||||||
currentVersion: string;
|
currentVersion?: string;
|
||||||
packageBase: string;
|
packageBase: string;
|
||||||
repository: RepositoryId;
|
repository: RepositoryId;
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,7 @@ export default function ArtifactsTab({
|
|||||||
})).reverse();
|
})).reverse();
|
||||||
},
|
},
|
||||||
queryKey: QueryKeys.artifacts(packageBase, repository),
|
queryKey: QueryKeys.artifacts(packageBase, repository),
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRollback = useCallback(async (version: string): Promise<void> => {
|
const handleRollback = useCallback(async (version: string): Promise<void> => {
|
||||||
@@ -101,7 +102,7 @@ export default function ArtifactsTab({
|
|||||||
<Tooltip title={params.row.version === currentVersion ? "Current version" : "Rollback to this version"}>
|
<Tooltip title={params.row.version === currentVersion ? "Current version" : "Rollback to this version"}>
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={params.row.version === currentVersion}
|
disabled={currentVersion === params.row.version}
|
||||||
onClick={() => void handleRollback(params.row.version)}
|
onClick={() => void handleRollback(params.row.version)}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default function BuildLogsTab({
|
|||||||
enabled: !!packageBase,
|
enabled: !!packageBase,
|
||||||
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
queryFn: () => client.fetch.fetchPackageLogs(packageBase, repository),
|
||||||
queryKey: QueryKeys.logs(packageBase, repository),
|
queryKey: QueryKeys.logs(packageBase, repository),
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build version selectors from all logs
|
// Build version selectors from all logs
|
||||||
@@ -116,6 +117,7 @@ export default function BuildLogsTab({
|
|||||||
)
|
)
|
||||||
: skipToken,
|
: skipToken,
|
||||||
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
queryKey: QueryKeys.logsVersion(packageBase, repository, activeVersion?.version ?? "", activeVersion?.processId ?? ""),
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive displayed logs: prefer fresh polled data when available
|
// Derive displayed logs: prefer fresh polled data when available
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export default function EventsTab({ packageBase, repository }: EventsTabProps):
|
|||||||
enabled: !!packageBase,
|
enabled: !!packageBase,
|
||||||
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
queryFn: () => client.fetch.fetchPackageEvents(repository, packageBase, 30),
|
||||||
queryKey: QueryKeys.events(repository, packageBase),
|
queryKey: QueryKeys.events(repository, packageBase),
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = useMemo<EventRow[]>(() => events.map((event, index) => ({
|
const rows = useMemo<EventRow[]>(() => events.map((event, index) => ({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function usePackageChanges(packageBase: string, repository: RepositoryId)
|
|||||||
enabled: !!packageBase,
|
enabled: !!packageBase,
|
||||||
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
queryFn: () => client.fetch.fetchPackageChanges(packageBase, repository),
|
||||||
queryKey: QueryKeys.changes(packageBase, repository),
|
queryKey: QueryKeys.changes(packageBase, repository),
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -93,6 +93,21 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def import_context(statement: nodes.Import | nodes.ImportFrom) -> tuple[nodes.NodeNG, str]:
|
||||||
|
"""
|
||||||
|
extract the lexical block containing an import
|
||||||
|
|
||||||
|
Args:
|
||||||
|
statement(nodes.Import | nodes.ImportFrom): import node
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[nodes.NodeNG, str]: parent node and its branch containing the import
|
||||||
|
"""
|
||||||
|
parent = statement.parent
|
||||||
|
field, _ = parent.locate_child(statement)
|
||||||
|
return parent, field
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]:
|
def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]:
|
||||||
"""
|
"""
|
||||||
@@ -192,7 +207,12 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
node(nodes.Module): module node to check
|
node(nodes.Module): module node to check
|
||||||
"""
|
"""
|
||||||
root_module, *_ = node.qname().split(".")
|
root_module, *_ = node.qname().split(".")
|
||||||
self.check_imports(self.imports(node.values()), root_module)
|
contexts: dict[tuple[nodes.NodeNG, str], list[nodes.Import | nodes.ImportFrom]] = {}
|
||||||
|
for statement in self.imports(node.values()):
|
||||||
|
contexts.setdefault(self.import_context(statement), []).append(statement)
|
||||||
|
|
||||||
|
for imports in contexts.values():
|
||||||
|
self.check_imports(imports, root_module)
|
||||||
|
|
||||||
|
|
||||||
def register(linter: PyLinter) -> None:
|
def register(linter: PyLinter) -> None:
|
||||||
|
|||||||
@@ -117,36 +117,12 @@ def get_package_status_extended(package: Package) -> dict[str, Any]:
|
|||||||
return {"status": BuildStatus().view(), "package": package.view()}
|
return {"status": BuildStatus().view(), "package": package.view()}
|
||||||
|
|
||||||
|
|
||||||
def import_error(package: str, components: list[str], mocker: MockerFixture) -> MagicMock:
|
|
||||||
"""
|
|
||||||
mock import error
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package(str): package name to import
|
|
||||||
components(list[str]): component to import if any (e.g. from ... import ...)
|
|
||||||
mocker(MockerFixture): mocker object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MagicMock: mocked object
|
|
||||||
"""
|
|
||||||
import builtins
|
|
||||||
_import = builtins.__import__
|
|
||||||
|
|
||||||
# pylint: disable=redefined-builtin
|
|
||||||
def test_import(name: str, globals: Any, locals: Any, from_list: list[str], level: Any):
|
|
||||||
if name == package and (not components or any(component in from_list for component in components)):
|
|
||||||
raise ImportError
|
|
||||||
return _import(name, globals, locals, from_list, level)
|
|
||||||
|
|
||||||
return mocker.patch.object(builtins, "__import__", test_import)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.hookimpl(trylast=True)
|
@pytest.hookimpl(trylast=True)
|
||||||
def pytest_configure() -> None:
|
def pytest_configure() -> None:
|
||||||
"""
|
"""
|
||||||
register helpers after pytest-helpers-namespace has initialized
|
register helpers after pytest-helpers-namespace has initialized
|
||||||
"""
|
"""
|
||||||
for helper in (anyvar, get_package_status, get_package_status_extended, import_error):
|
for helper in (anyvar, get_package_status, get_package_status_extended):
|
||||||
pytest.helpers.register(helper)
|
pytest.helpers.register(helper)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ env_list = [
|
|||||||
"check",
|
"check",
|
||||||
"tests",
|
"tests",
|
||||||
]
|
]
|
||||||
isolated_build = true
|
|
||||||
requires = [
|
requires = [
|
||||||
"tox-uv",
|
"tox-uv",
|
||||||
]
|
]
|
||||||
@@ -33,6 +32,17 @@ bandit = [
|
|||||||
"--configfile",
|
"--configfile",
|
||||||
".bandit.toml",
|
".bandit.toml",
|
||||||
]
|
]
|
||||||
|
coverage_report = [
|
||||||
|
"--show-missing",
|
||||||
|
"--skip-covered",
|
||||||
|
"--fail-under=100",
|
||||||
|
]
|
||||||
|
coverage_run = [
|
||||||
|
"--source",
|
||||||
|
"ahriman",
|
||||||
|
"-m",
|
||||||
|
"pytest",
|
||||||
|
]
|
||||||
manpage = [
|
manpage = [
|
||||||
"--author",
|
"--author",
|
||||||
"{[project]name} team",
|
"{[project]name} team",
|
||||||
@@ -274,15 +284,12 @@ deps = [
|
|||||||
{ replace = "ref", of = ["project", "extras"], extend = true },
|
{ replace = "ref", of = ["project", "extras"], extend = true },
|
||||||
]
|
]
|
||||||
pip_pre = true
|
pip_pre = true
|
||||||
recreate = true
|
|
||||||
commands = [
|
commands = [
|
||||||
[
|
[
|
||||||
"sphinx-build",
|
"sphinx-build",
|
||||||
"--builder",
|
"--builder",
|
||||||
"html",
|
"html",
|
||||||
"--fail-on-warning",
|
"--fail-on-warning",
|
||||||
"--jobs",
|
|
||||||
"1",
|
|
||||||
"--write-all",
|
"--write-all",
|
||||||
"docs",
|
"docs",
|
||||||
"{envtmpdir}/html",
|
"{envtmpdir}/html",
|
||||||
@@ -352,10 +359,7 @@ commands = [
|
|||||||
[
|
[
|
||||||
"coverage",
|
"coverage",
|
||||||
"run",
|
"run",
|
||||||
"--source",
|
{ replace = "ref", of = ["flags", "coverage_run"], extend = true },
|
||||||
"ahriman",
|
|
||||||
"-m",
|
|
||||||
"pytest",
|
|
||||||
{ replace = "posargs", default = [
|
{ replace = "posargs", default = [
|
||||||
"tests/test_tests.py",
|
"tests/test_tests.py",
|
||||||
"ahriman-core/tests",
|
"ahriman-core/tests",
|
||||||
@@ -366,9 +370,7 @@ commands = [
|
|||||||
[
|
[
|
||||||
"coverage",
|
"coverage",
|
||||||
"report",
|
"report",
|
||||||
"--show-missing",
|
{ replace = "ref", of = ["flags", "coverage_report"], extend = true },
|
||||||
"--skip-covered",
|
|
||||||
"--fail-under=100",
|
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user